{"repo": "baokaixina/simpletranslate", "files": {"neoforge/SimpleTranslate-NeoForge-1.20.6/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745665.3521395, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755586.2832036, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.1/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745665.4244397, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755578.9294214, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.20.3/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745665.9979556, "n": 32006, "h": "b3cf756d44702d28", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755582.9653976, "n": 22912, "h": "55128dae66f63229", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", true);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755584.3585308, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-26.2/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745666.1057742, "n": 13829, "h": "2980c34e39980424", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(\n format -> Component.literal(format.getDisplayName()),\n this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(\n value -> Component.literal(String.valueOf(value)),\n this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.20.6/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745666.3134596, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755587.6851447, "n": 12905, "h": "c5930b77f5c6ee03", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755589.0059495, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-26.1.1/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745666.6159759, "n": 32296, "h": "aad5b9f90664476b", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /**\n * When true, multi-region PUA-positioned HUD trees (including recognized\n * Wynncraft selector actionbars) keep the original. Default off: ordinary\n * layout HUDs retain their existing in-place path, while recognized Wynn\n * selectors use their dedicated source-anchored semantic renderer.\n */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_WYNN_OVERLAY_ENABLED = bool(\"hud.wynnOverlayEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-Forge-1.19.2/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745666.6477153, "n": 32006, "h": "b3cf756d44702d28", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755591.77841, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.20.1/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745666.7913995, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755573.360546, "n": 12905, "h": "c5930b77f5c6ee03", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755575.4165242, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.4/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745667.4315734, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755588.0513976, "n": 61591, "h": "6e9645a768c95133", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n private volatile ApiRequestProfile lastSuccessfulProfile;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = ModConfig.SOURCE_LANGUAGE.get();\n String targetLanguage = ModConfig.TARGET_LANGUAGE.get();\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface());\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n /** Output budget follows the Component JSON array, not the optional context preface. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int contextEnd = body.indexOf(\"[/CONTEXT]\");\n if (contextEnd >= 0) {\n body = body.substring(contextEnd + \"[/CONTEXT]\".length()).trim();\n }\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n SimpleTranslateMod.getLogger().debug(\n \"Translation API returned no assistant text; skipping identical automatic retry\");\n return null;\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n if (chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n && \"response.output_text.delta\".equals(chunk.get(\"type\").getAsString())\n && chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n lastSuccessfulProfile = profile;\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase();\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-Forge-1.18.2/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745667.473802, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.19.3/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745667.4751923, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755582.901896, "n": 61591, "h": "6e9645a768c95133", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n private volatile ApiRequestProfile lastSuccessfulProfile;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = ModConfig.SOURCE_LANGUAGE.get();\n String targetLanguage = ModConfig.TARGET_LANGUAGE.get();\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface());\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n /** Output budget follows the Component JSON array, not the optional context preface. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int contextEnd = body.indexOf(\"[/CONTEXT]\");\n if (contextEnd >= 0) {\n body = body.substring(contextEnd + \"[/CONTEXT]\".length()).trim();\n }\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n SimpleTranslateMod.getLogger().debug(\n \"Translation API returned no assistant text; skipping identical automatic retry\");\n return null;\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n if (chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n && \"response.output_text.delta\".equals(chunk.get(\"type\").getAsString())\n && chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n lastSuccessfulProfile = profile;\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase();\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.10/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745668.4357798, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755574.8628972, "n": 61591, "h": "6e9645a768c95133", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n private volatile ApiRequestProfile lastSuccessfulProfile;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = ModConfig.SOURCE_LANGUAGE.get();\n String targetLanguage = ModConfig.TARGET_LANGUAGE.get();\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface());\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n /** Output budget follows the Component JSON array, not the optional context preface. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int contextEnd = body.indexOf(\"[/CONTEXT]\");\n if (contextEnd >= 0) {\n body = body.substring(contextEnd + \"[/CONTEXT]\".length()).trim();\n }\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n SimpleTranslateMod.getLogger().debug(\n \"Translation API returned no assistant text; skipping identical automatic retry\");\n return null;\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n if (chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n && \"response.output_text.delta\".equals(chunk.get(\"type\").getAsString())\n && chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n lastSuccessfulProfile = profile;\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase();\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.2/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745668.8016798, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755584.4110928, "n": 61591, "h": "6e9645a768c95133", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n private volatile ApiRequestProfile lastSuccessfulProfile;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = ModConfig.SOURCE_LANGUAGE.get();\n String targetLanguage = ModConfig.TARGET_LANGUAGE.get();\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface());\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n /** Output budget follows the Component JSON array, not the optional context preface. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int contextEnd = body.indexOf(\"[/CONTEXT]\");\n if (contextEnd >= 0) {\n body = body.substring(contextEnd + \"[/CONTEXT]\".length()).trim();\n }\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n SimpleTranslateMod.getLogger().debug(\n \"Translation API returned no assistant text; skipping identical automatic retry\");\n return null;\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n if (chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n && \"response.output_text.delta\".equals(chunk.get(\"type\").getAsString())\n && chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n lastSuccessfulProfile = profile;\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase();\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-Forge-1.16.5/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745669.236202, "n": 62969, "h": "aa5ad84ae369c862", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = new com.google.gson.JsonParser().parse(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = new com.google.gson.JsonParser().parse(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = new com.google.gson.JsonParser().parse(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.size() == 0) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return new com.google.gson.JsonParser().parse(trimmed).isJsonArray()\n && new com.google.gson.JsonParser().parse(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (choices.size() != 0 && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (candidates.size() != 0 && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.20.5/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745669.3305576, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-26.1/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745671.8760848, "n": 13829, "h": "2980c34e39980424", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(\n format -> Component.literal(format.getDisplayName()),\n this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(\n value -> Component.literal(String.valueOf(value)),\n this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "d6109e84c914d30bb6062f84cc23872372da0280": {"t": 1785755571.4842412, "n": 12935, "h": "740080ccb93e4b01", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(\n format -> Component.literal(format.getDisplayName()),\n this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(\n value -> Component.literal(String.valueOf(value)),\n this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.6/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745672.2885394, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755571.2006426, "n": 12905, "h": "c5930b77f5c6ee03", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755572.4590552, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-26.1.2/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745672.357068, "n": 13829, "h": "2980c34e39980424", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(\n format -> Component.literal(format.getDisplayName()),\n this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(\n value -> Component.literal(String.valueOf(value)),\n this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "d6109e84c914d30bb6062f84cc23872372da0280": {"t": 1785755569.9145432, "n": 12935, "h": "740080ccb93e4b01", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(\n format -> Component.literal(format.getDisplayName()),\n this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(\n value -> Component.literal(String.valueOf(value)),\n this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "neoforge/SimpleTranslate-Forge-1.20.1/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745672.5177574, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755576.2302551, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.20.2/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745672.5477045, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.20.4/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745672.584252, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755580.0081944, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.5/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745672.590768, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755585.6133158, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.11/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745672.6748703, "n": 13829, "h": "2980c34e39980424", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(\n format -> Component.literal(format.getDisplayName()),\n this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(\n value -> Component.literal(String.valueOf(value)),\n this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755579.044832, "n": 11170, "h": "0ce34da593a85651", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(\n format -> Component.literal(format.getDisplayName()),\n this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(\n value -> Component.literal(String.valueOf(value)),\n this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.5/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745673.3609715, "n": 32296, "h": "aad5b9f90664476b", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /**\n * When true, multi-region PUA-positioned HUD trees (including recognized\n * Wynncraft selector actionbars) keep the original. Default off: ordinary\n * layout HUDs retain their existing in-place path, while recognized Wynn\n * selectors use their dedicated source-anchored semantic renderer.\n */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_WYNN_OVERLAY_ENABLED = bool(\"hud.wynnOverlayEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755579.8791618, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.6/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745673.374534, "n": 32296, "h": "aad5b9f90664476b", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /**\n * When true, multi-region PUA-positioned HUD trees (including recognized\n * Wynncraft selector actionbars) keep the original. Default off: ordinary\n * layout HUDs retain their existing in-place path, while recognized Wynn\n * selectors use their dedicated source-anchored semantic renderer.\n */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_WYNN_OVERLAY_ENABLED = bool(\"hud.wynnOverlayEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755587.3983166, "n": 22912, "h": "55128dae66f63229", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", true);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755588.9401088, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.11/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745673.5247197, "n": 32297, "h": "23953c6541d484af", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /**\n * When true, multi-region PUA-positioned HUD trees (including recognized\n * Wynncraft selector actionbars) keep the original. Default off: ordinary\n * layout HUDs retain their existing in-place path, while recognized Wynn\n * selectors use their dedicated source-anchored semantic renderer.\n */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_WYNN_OVERLAY_ENABLED = bool(\"hud.wynnOverlayEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755579.125626, "n": 20589, "h": "ef3303c893869766", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n\n\n"}}, "neoforge/SimpleTranslate-NeoForge-1.20.3/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745673.9585783, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.19.3/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745679.586477, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755566.6663864, "n": 12905, "h": "c5930b77f5c6ee03", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755567.9626808, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.20.4/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745679.7172046, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755578.4995925, "n": 12905, "h": "c5930b77f5c6ee03", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755579.6703384, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745680.0814276, "n": 32006, "h": "b3cf756d44702d28", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755581.6849396, "n": 22912, "h": "55128dae66f63229", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", true);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755583.2680907, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.4/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745680.089017, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755584.7236266, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.20.6/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745680.659129, "n": 32006, "h": "b3cf756d44702d28", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755579.443388, "n": 22912, "h": "55128dae66f63229", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", true);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755580.8390472, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.20.6/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745680.6990898, "n": 32006, "h": "b3cf756d44702d28", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755568.6860645, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745680.7743623, "n": 32006, "h": "b3cf756d44702d28", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745682.0194845, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.7/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745682.17453, "n": 32296, "h": "aad5b9f90664476b", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /**\n * When true, multi-region PUA-positioned HUD trees (including recognized\n * Wynncraft selector actionbars) keep the original. Default off: ordinary\n * layout HUDs retain their existing in-place path, while recognized Wynn\n * selectors use their dedicated source-anchored semantic renderer.\n */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_WYNN_OVERLAY_ENABLED = bool(\"hud.wynnOverlayEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.20.6/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745683.0350893, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.19.2/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745683.6757197, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755578.7456648, "n": 61591, "h": "6e9645a768c95133", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n private volatile ApiRequestProfile lastSuccessfulProfile;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = ModConfig.SOURCE_LANGUAGE.get();\n String targetLanguage = ModConfig.TARGET_LANGUAGE.get();\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface());\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n /** Output budget follows the Component JSON array, not the optional context preface. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int contextEnd = body.indexOf(\"[/CONTEXT]\");\n if (contextEnd >= 0) {\n body = body.substring(contextEnd + \"[/CONTEXT]\".length()).trim();\n }\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n SimpleTranslateMod.getLogger().debug(\n \"Translation API returned no assistant text; skipping identical automatic retry\");\n return null;\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n if (chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n && \"response.output_text.delta\".equals(chunk.get(\"type\").getAsString())\n && chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n lastSuccessfulProfile = profile;\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase();\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.20.6/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745683.692323, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755570.941851, "n": 61591, "h": "6e9645a768c95133", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n private volatile ApiRequestProfile lastSuccessfulProfile;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = ModConfig.SOURCE_LANGUAGE.get();\n String targetLanguage = ModConfig.TARGET_LANGUAGE.get();\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface());\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n /** Output budget follows the Component JSON array, not the optional context preface. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int contextEnd = body.indexOf(\"[/CONTEXT]\");\n if (contextEnd >= 0) {\n body = body.substring(contextEnd + \"[/CONTEXT]\".length()).trim();\n }\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n SimpleTranslateMod.getLogger().debug(\n \"Translation API returned no assistant text; skipping identical automatic retry\");\n return null;\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n if (chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n && \"response.output_text.delta\".equals(chunk.get(\"type\").getAsString())\n && chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n lastSuccessfulProfile = profile;\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase();\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.1/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745687.5266798, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755586.2203534, "n": 12905, "h": "c5930b77f5c6ee03", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755587.6397765, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.20.1/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745687.7619452, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755581.8295193, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-26.2/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745688.3248253, "n": 32296, "h": "aad5b9f90664476b", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /**\n * When true, multi-region PUA-positioned HUD trees (including recognized\n * Wynncraft selector actionbars) keep the original. Default off: ordinary\n * layout HUDs retain their existing in-place path, while recognized Wynn\n * selectors use their dedicated source-anchored semantic renderer.\n */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_WYNN_OVERLAY_ENABLED = bool(\"hud.wynnOverlayEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755565.490424, "n": 22912, "h": "55128dae66f63229", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", true);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "d6109e84c914d30bb6062f84cc23872372da0280": {"t": 1785755567.346496, "n": 22798, "h": "2792b6fbac4ae43c", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", true);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.20.2/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745688.3717656, "n": 32006, "h": "b3cf756d44702d28", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755581.8743184, "n": 22912, "h": "55128dae66f63229", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", true);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755583.3565004, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.19.3/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745688.440451, "n": 32006, "h": "b3cf756d44702d28", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755581.168, "n": 22912, "h": "55128dae66f63229", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", true);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755583.1349924, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.6/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745688.667833, "n": 32296, "h": "aad5b9f90664476b", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /**\n * When true, multi-region PUA-positioned HUD trees (including recognized\n * Wynncraft selector actionbars) keep the original. Default off: ordinary\n * layout HUDs retain their existing in-place path, while recognized Wynn\n * selectors use their dedicated source-anchored semantic renderer.\n */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_WYNN_OVERLAY_ENABLED = bool(\"hud.wynnOverlayEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.7/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745688.6841316, "n": 32296, "h": "aad5b9f90664476b", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /**\n * When true, multi-region PUA-positioned HUD trees (including recognized\n * Wynncraft selector actionbars) keep the original. Default off: ordinary\n * layout HUDs retain their existing in-place path, while recognized Wynn\n * selectors use their dedicated source-anchored semantic renderer.\n */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_WYNN_OVERLAY_ENABLED = bool(\"hud.wynnOverlayEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755573.7325478, "n": 22912, "h": "55128dae66f63229", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", true);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755575.1351242, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.1/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745688.7411957, "n": 32006, "h": "b3cf756d44702d28", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755585.90958, "n": 22912, "h": "55128dae66f63229", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", true);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755587.547785, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.20.1/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745689.4513237, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755572.948219, "n": 61591, "h": "6e9645a768c95133", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n private volatile ApiRequestProfile lastSuccessfulProfile;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = ModConfig.SOURCE_LANGUAGE.get();\n String targetLanguage = ModConfig.TARGET_LANGUAGE.get();\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface());\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n /** Output budget follows the Component JSON array, not the optional context preface. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int contextEnd = body.indexOf(\"[/CONTEXT]\");\n if (contextEnd >= 0) {\n body = body.substring(contextEnd + \"[/CONTEXT]\".length()).trim();\n }\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n SimpleTranslateMod.getLogger().debug(\n \"Translation API returned no assistant text; skipping identical automatic retry\");\n return null;\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n if (chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n && \"response.output_text.delta\".equals(chunk.get(\"type\").getAsString())\n && chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n lastSuccessfulProfile = profile;\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase();\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-26.1.2/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745689.5896623, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}, "d6109e84c914d30bb6062f84cc23872372da0280": {"t": 1785755572.916964, "n": 61591, "h": "6e9645a768c95133", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n private volatile ApiRequestProfile lastSuccessfulProfile;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = ModConfig.SOURCE_LANGUAGE.get();\n String targetLanguage = ModConfig.TARGET_LANGUAGE.get();\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface());\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n /** Output budget follows the Component JSON array, not the optional context preface. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int contextEnd = body.indexOf(\"[/CONTEXT]\");\n if (contextEnd >= 0) {\n body = body.substring(contextEnd + \"[/CONTEXT]\".length()).trim();\n }\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n SimpleTranslateMod.getLogger().debug(\n \"Translation API returned no assistant text; skipping identical automatic retry\");\n return null;\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n if (chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n && \"response.output_text.delta\".equals(chunk.get(\"type\").getAsString())\n && chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n lastSuccessfulProfile = profile;\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase();\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-Forge-1.20.1/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745689.5939374, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-Forge-1.19.2/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745689.6196597, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.5/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745690.323553, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-26.2/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745692.9219708, "n": 13829, "h": "2980c34e39980424", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(\n format -> Component.literal(format.getDisplayName()),\n this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(\n value -> Component.literal(String.valueOf(value)),\n this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "d6109e84c914d30bb6062f84cc23872372da0280": {"t": 1785755584.3230898, "n": 12935, "h": "740080ccb93e4b01", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(\n format -> Component.literal(format.getDisplayName()),\n this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(\n value -> Component.literal(String.valueOf(value)),\n this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.19.4/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745693.1454782, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755567.3140624, "n": 12905, "h": "c5930b77f5c6ee03", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755570.1539547, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.20.5/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745693.396757, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.10/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745693.5239153, "n": 13765, "h": "f98f3dc50178ab91", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755579.9341915, "n": 11140, "h": "962714aa9e3422ca", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.20.1/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745694.1101217, "n": 32006, "h": "b3cf756d44702d28", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755582.9561908, "n": 22912, "h": "55128dae66f63229", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", true);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755584.345389, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.2/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745694.115331, "n": 32006, "h": "b3cf756d44702d28", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755582.769147, "n": 22912, "h": "55128dae66f63229", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", true);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755584.2822556, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.7/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745694.1299334, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755565.367376, "n": 12905, "h": "c5930b77f5c6ee03", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755566.7607033, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.7/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745694.1665347, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.20.2/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745694.838505, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755583.4728787, "n": 61591, "h": "6e9645a768c95133", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n private volatile ApiRequestProfile lastSuccessfulProfile;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = ModConfig.SOURCE_LANGUAGE.get();\n String targetLanguage = ModConfig.TARGET_LANGUAGE.get();\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface());\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n /** Output budget follows the Component JSON array, not the optional context preface. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int contextEnd = body.indexOf(\"[/CONTEXT]\");\n if (contextEnd >= 0) {\n body = body.substring(contextEnd + \"[/CONTEXT]\".length()).trim();\n }\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n SimpleTranslateMod.getLogger().debug(\n \"Translation API returned no assistant text; skipping identical automatic retry\");\n return null;\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n if (chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n && \"response.output_text.delta\".equals(chunk.get(\"type\").getAsString())\n && chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n lastSuccessfulProfile = profile;\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase();\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.20.5/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745694.9813075, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755568.9682684, "n": 61591, "h": "6e9645a768c95133", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n private volatile ApiRequestProfile lastSuccessfulProfile;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = ModConfig.SOURCE_LANGUAGE.get();\n String targetLanguage = ModConfig.TARGET_LANGUAGE.get();\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface());\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n /** Output budget follows the Component JSON array, not the optional context preface. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int contextEnd = body.indexOf(\"[/CONTEXT]\");\n if (contextEnd >= 0) {\n body = body.substring(contextEnd + \"[/CONTEXT]\".length()).trim();\n }\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n SimpleTranslateMod.getLogger().debug(\n \"Translation API returned no assistant text; skipping identical automatic retry\");\n return null;\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n if (chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n && \"response.output_text.delta\".equals(chunk.get(\"type\").getAsString())\n && chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n lastSuccessfulProfile = profile;\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase();\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.8/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745695.0688832, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-26.1.2/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745695.1575572, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-26.2/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745695.3055449, "n": 32296, "h": "aad5b9f90664476b", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /**\n * When true, multi-region PUA-positioned HUD trees (including recognized\n * Wynncraft selector actionbars) keep the original. Default off: ordinary\n * layout HUDs retain their existing in-place path, while recognized Wynn\n * selectors use their dedicated source-anchored semantic renderer.\n */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_WYNN_OVERLAY_ENABLED = bool(\"hud.wynnOverlayEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.7/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745695.339136, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-26.1.1/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745695.7504005, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.20/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745700.6217973, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755569.9457142, "n": 12905, "h": "c5930b77f5c6ee03", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755572.4089255, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.10/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745701.4883575, "n": 13765, "h": "f98f3dc50178ab91", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755578.51319, "n": 12905, "h": "6387f27044821727", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755579.7226388, "n": 11140, "h": "962714aa9e3422ca", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.11/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745701.669563, "n": 13829, "h": "2980c34e39980424", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(\n format -> Component.literal(format.getDisplayName()),\n this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(\n value -> Component.literal(String.valueOf(value)),\n this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755573.8933094, "n": 12935, "h": "740080ccb93e4b01", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(\n format -> Component.literal(format.getDisplayName()),\n this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(\n value -> Component.literal(String.valueOf(value)),\n this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755575.2452314, "n": 11170, "h": "0ce34da593a85651", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.addFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(\n format -> Component.literal(format.getDisplayName()),\n this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(\n value -> Component.literal(String.valueOf(value)),\n this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.20.5/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745701.7231927, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755570.6229038, "n": 12905, "h": "c5930b77f5c6ee03", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755571.7889485, "n": 11140, "h": "d5e039de3340c427", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.translation.TranslationService;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName()))\n .withValues(ApiFormat.values())\n .withInitialValue(this.currentApiFormat)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value)))\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .withInitialValue(this.currentMaxParallelRequests)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationService.ModelAccessResult result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(ModConfig.normalizeApiUrl(this.currentApiUrl));\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId));\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(false);\n SimpleTranslateMod.getLogger().info(\"Model settings saved format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.16.5/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745702.0644295, "n": 32198, "h": "1f3456a509c6eb12", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = new com.google.gson.JsonParser().parse(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = com.yourname.simpletranslate.core.JsonCompat.deepCopy(root);\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : com.yourname.simpletranslate.core.JsonCompat.deepCopy(loadedRoot);\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = com.yourname.simpletranslate.core.JsonCompat.deepCopy(root);\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, com.yourname.simpletranslate.core.JsonCompat.deepCopy(root.get(oldKey)));\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.20.4/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745702.485354, "n": 32006, "h": "b3cf756d44702d28", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755576.6147697, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.10/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745702.7343647, "n": 32296, "h": "aad5b9f90664476b", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /**\n * When true, multi-region PUA-positioned HUD trees (including recognized\n * Wynncraft selector actionbars) keep the original. Default off: ordinary\n * layout HUDs retain their existing in-place path, while recognized Wynn\n * selectors use their dedicated source-anchored semantic renderer.\n */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_WYNN_OVERLAY_ENABLED = bool(\"hud.wynnOverlayEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755579.551858, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.3/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745702.7854917, "n": 32006, "h": "b3cf756d44702d28", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.2/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745702.818885, "n": 32006, "h": "b3cf756d44702d28", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-Forge-1.16.5/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745702.9043007, "n": 32198, "h": "1f3456a509c6eb12", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = new com.google.gson.JsonParser().parse(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = com.yourname.simpletranslate.core.JsonCompat.deepCopy(root);\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : com.yourname.simpletranslate.core.JsonCompat.deepCopy(loadedRoot);\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = com.yourname.simpletranslate.core.JsonCompat.deepCopy(root);\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, com.yourname.simpletranslate.core.JsonCompat.deepCopy(root.get(oldKey)));\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-Forge-1.12.2/src/main/java/com/yourname/simpletranslate/translation/TranslationEngine.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745703.1956139, "n": 106128, "h": "531be72d129131c7", "c": "package com.yourname.simpletranslate.translation;\n\nimport com.yourname.simpletranslate.SimpleTranslateForge1122;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.cache.TermDictionary;\nimport com.yourname.simpletranslate.cache.TranslationBlacklist;\nimport com.yourname.simpletranslate.cache.TranslationCache;\nimport com.yourname.simpletranslate.cache.LineTranslationMemory;\nimport com.yourname.simpletranslate.cache.SharedCacheEntry;\nimport com.yourname.simpletranslate.config.TranslationProfileManager;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.Surface;\nimport com.yourname.simpletranslate.keybind.HoldOriginalState;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.feature.tooltip.TooltipTranslationGlowRenderer;\nimport com.yourname.simpletranslate.feature.tooltip.TooltipRequestTriggerState;\nimport com.yourname.simpletranslate.core.TextContextMemory;\nimport com.yourname.simpletranslate.core.DirectSurfaceTranslator;\nimport com.yourname.simpletranslate.core.ComponentTranslationResult;\nimport com.yourname.simpletranslate.core.ComponentListTranslationResult;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\nimport com.yourname.simpletranslate.core.DynamicTextTemplate;\nimport com.yourname.simpletranslate.transport.TokenUsageMonitor;\nimport com.yourname.simpletranslate.transport.JsonPassthroughPrompts;\nimport net.minecraft.util.text.ITextComponent;\nimport net.minecraft.util.text.TextComponentString;\n\nimport java.io.BufferedReader;\nimport java.io.File;\nimport java.io.FileInputStream;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.io.OutputStream;\nimport java.net.HttpURLConnection;\nimport java.net.URL;\nimport java.nio.charset.StandardCharsets;\nimport java.security.MessageDigest;\nimport java.util.HashMap;\nimport java.util.ArrayList;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Properties;\nimport java.util.concurrent.CancellationException;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.ThreadFactory;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.concurrent.atomic.AtomicReference;\nimport java.util.function.Function;\nimport java.util.function.Supplier;\n\n/**\n * Java-8 implementation of the product Component-JSON pipeline for 1.12.2.\n *\n *

Every game-text request is a top-level JSON\n * array of Minecraft components and every cached value is marked\n * {@code component_json_v1} in the {@code stx2} store.

\n */\npublic final class TranslationEngine implements TranslationService {\n public static final String CACHE_NAMESPACE = \"stx2\";\n public static final String COMPONENT_JSON_FORMAT = \"component_json_v1\";\n private static final String DEFAULT_ENDPOINT = \"https://api.deepseek.com/chat/completions\";\n private static final String DEFAULT_MODEL = \"deepseek-chat\";\n private static final int MIN_REQUEST_WORKERS = 1;\n private static final int MAX_REQUEST_WORKERS = 8;\n private static final String[] TEXT_CONTEXT_SCOPES = {\n \"received_chat\", \"sent_chat\", \"item_tooltip\", \"hover_tooltip\", \"book\", \"sign\",\n \"hud_captions\", \"hud_progress\", \"entity_name\"\n };\n\n private final File configFile;\n private final File dataRoot;\n private final TranslationBlacklist translationBlacklist;\n private TermDictionary termDictionary;\n private LineTranslationMemory lineTranslationMemory;\n private TranslationCache translationCache;\n private final TranslationRequestQueue requestQueue = new TranslationRequestQueue();\n private final ExecutorService httpExecutor = Executors.newFixedThreadPool(\n MAX_REQUEST_WORKERS, new ThreadFactory() {\n private final AtomicInteger nextId = new AtomicInteger(1);\n @Override public Thread newThread(Runnable runnable) {\n Thread thread = new Thread(runnable,\n \"SimpleTranslate-1.12.2-HTTP-\" + nextId.getAndIncrement());\n thread.setDaemon(true);\n return thread;\n }\n });\n /** Read only during one-time migration from the retired properties file. */\n private final Map legacyFeatureEnabled = new HashMap();\n private final Map legacyTextContextScopeEnabled = new HashMap();\n private String endpoint = DEFAULT_ENDPOINT;\n private String apiKey = \"\";\n private String model = DEFAULT_MODEL;\n private String sourceLanguage = \"auto\";\n private String targetLanguage = \"Chinese\";\n private int maxParallelRequests = 5;\n /** Baseline default is BUTTON; AUTO retains the collect-window path. */\n private boolean chatAutoMode;\n private boolean holdOriginalEnabled;\n private boolean manualTooltipMode;\n private boolean outgoingChatEnabled;\n private boolean tooltipGlowEnabled;\n private boolean tokenMonitorEnabled;\n private boolean textContextEnabled = true;\n private int textContextMessageCount = 6;\n private volatile String lastRequestFailure = \"\";\n\n public TranslationEngine(File configFile) {\n this.configFile = configFile;\n File parent = configFile.getParentFile();\n this.dataRoot = new File(parent, \"simple_translate\");\n migrateLegacyDataFile(new File(parent, \"simple_translate-blacklist.json\"), new File(dataRoot, \"blacklist.json\"));\n this.translationBlacklist = new TranslationBlacklist(new File(dataRoot, \"blacklist.json\"));\n this.termDictionary = termsForScope(\"global\");\n this.lineTranslationMemory = lineMemoryForScope(\"global\");\n migrateLegacyDataFile(new File(new File(new File(dataRoot, \"cache\"), \"global\"), \"translations.json\"),\n new File(dataRoot, \"cache.json\"));\n this.translationCache = cacheForScope(\"global\");\n TranslationProfileManager.init(dataRoot);\n load();\n applyRequestWorkerCount();\n translationCache.load();\n translationBlacklist.load();\n termDictionary.load();\n lineTranslationMemory.load();\n }\n\n public synchronized boolean isConfigured() {\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA\n ? !endpoint.trim().isEmpty() : !apiKey.trim().isEmpty();\n }\n\n public synchronized String getEndpoint() { return endpoint; }\n public synchronized String getApiKey() { return apiKey; }\n public synchronized String getModel() { return model; }\n public synchronized String getSourceLanguage() { return sourceLanguage; }\n public synchronized String getTargetLanguage() { return targetLanguage; }\n public synchronized int getMaxParallelRequests() { return maxParallelRequests; }\n public synchronized boolean isChatAutoMode() { return ModConfig.CHAT_MODE.get() == ModConfig.TranslationMode.AUTO; }\n public synchronized void setChatAutoMode(boolean enabled) {\n ModConfig.CHAT_MODE.set(enabled ? ModConfig.TranslationMode.AUTO : ModConfig.TranslationMode.BUTTON);\n ModConfig.save();\n }\n public synchronized boolean isHoldOriginalEnabled() { return ModConfig.HOLD_ORIGINAL_ENABLED.get(); }\n public synchronized void setHoldOriginalEnabled(boolean enabled) {\n ModConfig.HOLD_ORIGINAL_ENABLED.set(enabled);\n ModConfig.save();\n }\n public synchronized boolean isManualTooltipMode() {\n return ModConfig.TOOLTIP_ITEM_TRIGGER_MODE.get() == ModConfig.TooltipTriggerMode.SHORTCUT;\n }\n public synchronized void setManualTooltipMode(boolean enabled) {\n ModConfig.TOOLTIP_ITEM_TRIGGER_MODE.set(enabled\n ? ModConfig.TooltipTriggerMode.SHORTCUT : ModConfig.TooltipTriggerMode.HOVER);\n if (!enabled) TooltipRequestTriggerState.clear();\n ModConfig.save();\n }\n public void requestManualTooltip() { TooltipRequestTriggerState.armShortcutRequest(); }\n public synchronized boolean isOutgoingChatEnabled() { return ModConfig.CHAT_OUTGOING_ENABLED.get(); }\n public synchronized void setOutgoingChatEnabled(boolean enabled) {\n ModConfig.CHAT_OUTGOING_ENABLED.set(enabled); ModConfig.save();\n }\n public synchronized boolean isTooltipGlowEnabled() { return ModConfig.TOOLTIP_GLOW_ENABLED.get(); }\n public synchronized void setTooltipGlowEnabled(boolean enabled) {\n ModConfig.TOOLTIP_GLOW_ENABLED.set(enabled); ModConfig.save();\n }\n public synchronized boolean isTokenMonitorEnabled() { return ModConfig.TOKEN_MONITOR_ENABLED.get(); }\n public synchronized void setTokenMonitorEnabled(boolean enabled) {\n ModConfig.TOKEN_MONITOR_ENABLED.set(enabled); ModConfig.save();\n }\n public synchronized boolean isTextContextEnabled() { return ModConfig.API_TEXT_CONTEXT_ENABLED.get(); }\n public synchronized void setTextContextEnabled(boolean enabled) {\n ModConfig.API_TEXT_CONTEXT_ENABLED.set(enabled); TextContextMemory.settingsChanged(); ModConfig.save();\n }\n public synchronized int getTextContextMessageCount() { return ModConfig.CHAT_CONTEXT_MESSAGE_COUNT.get(); }\n public synchronized void setTextContextMessageCount(int count) {\n ModConfig.CHAT_CONTEXT_MESSAGE_COUNT.set(Math.max(0, Math.min(20, count)));\n ModConfig.save();\n }\n public synchronized boolean isTextContextScopeEnabled(String scope) {\n String normalized = normalizeContextScope(scope);\n if (\"received_chat\".equals(normalized)) return ModConfig.API_TEXT_CONTEXT_RECEIVED_CHAT.get();\n if (\"sent_chat\".equals(normalized)) return ModConfig.API_TEXT_CONTEXT_SENT_CHAT.get();\n if (\"item_tooltip\".equals(normalized)) return ModConfig.API_TEXT_CONTEXT_ITEM_TOOLTIP.get();\n if (\"hover_tooltip\".equals(normalized)) return ModConfig.API_TEXT_CONTEXT_HOVER_TOOLTIP.get();\n if (\"book\".equals(normalized)) return ModConfig.API_TEXT_CONTEXT_BOOK.get();\n if (\"sign\".equals(normalized)) return ModConfig.API_TEXT_CONTEXT_SIGN.get();\n if (\"hud_captions\".equals(normalized)) return ModConfig.API_TEXT_CONTEXT_HUD_CAPTIONS.get();\n if (\"hud_progress\".equals(normalized)) return ModConfig.API_TEXT_CONTEXT_HUD_PROGRESS.get();\n if (\"entity_name\".equals(normalized)) return ModConfig.API_TEXT_CONTEXT_ENTITY_NAME.get();\n return true;\n }\n public synchronized void setTextContextScopeEnabled(String scope, boolean enabled) {\n String normalized = normalizeContextScope(scope);\n if (\"received_chat\".equals(normalized)) ModConfig.API_TEXT_CONTEXT_RECEIVED_CHAT.set(enabled);\n else if (\"sent_chat\".equals(normalized)) ModConfig.API_TEXT_CONTEXT_SENT_CHAT.set(enabled);\n else if (\"item_tooltip\".equals(normalized)) ModConfig.API_TEXT_CONTEXT_ITEM_TOOLTIP.set(enabled);\n else if (\"hover_tooltip\".equals(normalized)) ModConfig.API_TEXT_CONTEXT_HOVER_TOOLTIP.set(enabled);\n else if (\"book\".equals(normalized)) ModConfig.API_TEXT_CONTEXT_BOOK.set(enabled);\n else if (\"sign\".equals(normalized)) ModConfig.API_TEXT_CONTEXT_SIGN.set(enabled);\n else if (\"hud_captions\".equals(normalized)) ModConfig.API_TEXT_CONTEXT_HUD_CAPTIONS.set(enabled);\n else if (\"hud_progress\".equals(normalized)) ModConfig.API_TEXT_CONTEXT_HUD_PROGRESS.set(enabled);\n else if (\"entity_name\".equals(normalized)) ModConfig.API_TEXT_CONTEXT_ENTITY_NAME.set(enabled);\n TextContextMemory.settingsChanged();\n ModConfig.save();\n }\n\n /** A disabled scope neither contributes examples nor receives them in its prompt. */\n public synchronized boolean isTextContextAllowedForSurface(String surface) {\n return ModConfig.API_TEXT_CONTEXT_ENABLED.get() && isTextContextScopeEnabled(contextScopeForSurface(surface));\n }\n\n public static String[] textContextScopes() {\n return TEXT_CONTEXT_SCOPES.clone();\n }\n\n /** Bound the Java-8 HTTP executor exactly as the baseline request setting does. */\n public synchronized void setMaxParallelRequests(int workers) {\n maxParallelRequests = clampWorkers(workers);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(maxParallelRequests);\n applyRequestWorkerCount();\n ModConfig.save();\n }\n public TranslationBlacklist getTranslationBlacklist() { return translationBlacklist; }\n public TermDictionary getTermDictionary() { return termDictionary; }\n public TranslationCache getTranslationCache() { return translationCache; }\n public LineTranslationMemory getLineTranslationMemory() { return lineTranslationMemory; }\n\n public synchronized void switchCacheScope(String scopeId) {\n String scope = scopeId == null || scopeId.trim().isEmpty() ? \"global\" : scopeId;\n if (translationCache != null) translationCache.flush();\n if (lineTranslationMemory != null) lineTranslationMemory.flush();\n if (termDictionary != null) termDictionary.save();\n translationCache = cacheForScope(scope);\n translationCache.load();\n lineTranslationMemory = lineMemoryForScope(scope);\n lineTranslationMemory.load();\n termDictionary = termsForScope(scope);\n termDictionary.load();\n requestQueue.clear();\n TextContextMemory.clear();\n }\n\n public synchronized void migrateLegacyScope(String legacyScopeId, String targetScopeId) {\n String legacy = legacyScopeId == null ? \"\" : legacyScopeId.trim();\n String target = targetScopeId == null ? \"\" : targetScopeId.trim();\n if (legacy.isEmpty() || target.isEmpty() || legacy.equals(target)) return;\n File legacyCacheDir = new File(new File(dataRoot, \"cache\"), legacy);\n if (!legacyCacheDir.isDirectory()) return;\n TranslationCache targetCache = cacheForScope(target);\n targetCache.load();\n File legacyTranslations = new File(legacyCacheDir, \"translations.json\");\n if (legacyTranslations.isFile()) {\n try { targetCache.importFromFile(legacyTranslations.toPath(), true); }\n catch (java.io.IOException error) {\n SimpleTranslateForge1122.getLogger().warn(\"Failed to migrate legacy cache scope {} to {}\", legacy, target, error);\n }\n }\n targetCache.flush();\n LineTranslationMemory targetMemory = lineMemoryForScope(target);\n targetMemory.load();\n targetMemory.mergeFrom(new File(legacyCacheDir, \"line_memory.json\").toPath());\n targetMemory.flush();\n File legacyTerms = new File(new File(new File(dataRoot, \"terms\"), legacy), \"terms.json\");\n File targetTerms = new File(new File(new File(dataRoot, \"terms\"), target), \"terms.json\");\n migrateLegacyDataFile(legacyTerms, targetTerms);\n }\n\n private TranslationCache cacheForScope(String scope) {\n if (\"global\".equals(scope)) return new TranslationCache(new File(dataRoot, \"cache.json\").toPath());\n return new TranslationCache(new File(new File(new File(dataRoot, \"cache\"), scope), \"translations.json\").toPath());\n }\n\n private LineTranslationMemory lineMemoryForScope(String scope) {\n if (\"global\".equals(scope)) return new LineTranslationMemory(new File(dataRoot, \"line_memory.json\").toPath());\n return new LineTranslationMemory(new File(new File(new File(dataRoot, \"cache\"), scope), \"line_memory.json\").toPath());\n }\n\n private TermDictionary termsForScope(String scope) {\n File target = new File(new File(new File(dataRoot, \"terms\"), scope), \"terms.json\");\n if (\"global\".equals(scope)) {\n migrateLegacyDataFile(new File(configFile.getParentFile(), \"simple_translate-terms.json\"), target);\n migrateLegacyDataFile(new File(dataRoot, \"terms.json\"), target);\n }\n return new TermDictionary(target);\n }\n\n private static void migrateLegacyDataFile(File source, File target) {\n if (source == null || target == null || target.exists() || !source.isFile()) return;\n try {\n File parent = target.getParentFile();\n if (parent != null) parent.mkdirs();\n java.nio.file.Files.copy(source.toPath(), target.toPath());\n } catch (Exception ignored) {\n // Legacy data is left untouched and can be retried on the next startup.\n }\n }\n\n /** Shared imports are additive and intentionally never change a local cache value. */\n public synchronized void importSharedCacheEntries(Map incoming) {\n if (incoming == null || incoming.isEmpty()) return;\n boolean changed = false;\n for (Map.Entry entry : incoming.entrySet()) {\n if (isShareableComponentEntry(entry.getKey(), entry.getValue())\n && translationCache.putSharedIfAbsent(entry.getKey(), entry.getValue(), \"\", \"\",\n false, System.currentTimeMillis(), 0L)) {\n changed = true;\n }\n }\n if (changed && isFeatureEnabled(\"cache\")) translationCache.save();\n }\n\n public synchronized int importSharedCacheEntries(List incoming) {\n if (incoming == null || incoming.isEmpty()) return 0;\n int imported = 0;\n for (SharedCacheEntry entry : incoming) {\n if (entry != null && translationCache.putSharedIfAbsent(entry.key(), entry.translation(),\n entry.sourceText(), entry.translationText(), entry.editedByPlayer(),\n entry.createdAt(), entry.editedAt())) {\n imported++;\n }\n }\n if (imported > 0 && isFeatureEnabled(\"cache\")) translationCache.save();\n return imported;\n }\n\n /** Imported shared values never re-upload unless a local translation later replaces them. */\n public synchronized Map snapshotLocalCacheEntries() {\n Map result = new HashMap();\n for (TranslationCache.CacheViewEntry entry : translationCache.getEntries().values()) {\n if (!entry.sharedImported() && isShareableComponentEntry(entry.key(), entry.translation())) {\n result.put(entry.key(), entry.translation());\n }\n }\n return result;\n }\n\n public boolean isBlacklisted(ITextComponent component) {\n return component != null && translationBlacklist.contains(component.getUnformattedText());\n }\n\n public boolean containsBlacklistedText(String text) {\n return translationBlacklist != null && translationBlacklist.containsBlacklistedEntry(text);\n }\n\n public synchronized boolean isFeatureEnabled(String feature) {\n if (\"global\".equals(feature)) return ModConfig.GLOBAL_ENABLED.get();\n if (\"chat\".equals(feature)) return ModConfig.CHAT_ENABLED.get();\n if (\"tooltip_item\".equals(feature)) return ModConfig.TOOLTIP_ITEM_ENABLED.get();\n if (\"tooltip_hover\".equals(feature)) return ModConfig.TOOLTIP_CHAT_HOVER_ENABLED.get();\n if (\"book\".equals(feature)) return ModConfig.CONTENT_BOOK_ENABLED.get();\n if (\"sign\".equals(feature)) return ModConfig.CONTENT_SIGN_ENABLED.get();\n if (\"advancement\".equals(feature)) return ModConfig.CONTENT_ADVANCEMENT_ENABLED.get();\n if (\"entity_name\".equals(feature)) return ModConfig.CONTENT_ENTITY_NAME_ENABLED.get();\n if (\"hud_scoreboard\".equals(feature)) return ModConfig.HUD_SCOREBOARD_ENABLED.get();\n if (\"hud_bossbar\".equals(feature)) return ModConfig.HUD_BOSSBAR_ENABLED.get();\n if (\"hud_title\".equals(feature)) return ModConfig.HUD_TITLE_ENABLED.get();\n if (\"hud_actionbar\".equals(feature)) return ModConfig.HUD_ACTIONBAR_ENABLED.get();\n if (\"ftb\".equals(feature)) return ModConfig.MOD_TRANSLATION_ENABLED.get() && ModConfig.MOD_FTB_QUESTS_ENABLED.get();\n if (\"tips\".equals(feature)) return ModConfig.MOD_TRANSLATION_ENABLED.get() && ModConfig.MOD_TIPS_ENABLED.get();\n if (\"gui\".equals(feature)) return ModConfig.CONTENT_GUI_ENABLED.get();\n if (\"cache\".equals(feature)) return ModConfig.CACHE_ENABLED.get();\n return true;\n }\n\n public synchronized void setFeatureEnabled(String feature, boolean enabled) {\n if (feature == null || feature.trim().isEmpty()) return;\n if (\"global\".equals(feature)) ModConfig.GLOBAL_ENABLED.set(enabled);\n else if (\"chat\".equals(feature)) ModConfig.CHAT_ENABLED.set(enabled);\n else if (\"tooltip_item\".equals(feature)) ModConfig.TOOLTIP_ITEM_ENABLED.set(enabled);\n else if (\"tooltip_hover\".equals(feature)) ModConfig.TOOLTIP_CHAT_HOVER_ENABLED.set(enabled);\n else if (\"book\".equals(feature)) ModConfig.CONTENT_BOOK_ENABLED.set(enabled);\n else if (\"sign\".equals(feature)) ModConfig.CONTENT_SIGN_ENABLED.set(enabled);\n else if (\"advancement\".equals(feature)) ModConfig.CONTENT_ADVANCEMENT_ENABLED.set(enabled);\n else if (\"entity_name\".equals(feature)) ModConfig.CONTENT_ENTITY_NAME_ENABLED.set(enabled);\n else if (\"hud_scoreboard\".equals(feature)) ModConfig.HUD_SCOREBOARD_ENABLED.set(enabled);\n else if (\"hud_bossbar\".equals(feature)) ModConfig.HUD_BOSSBAR_ENABLED.set(enabled);\n else if (\"hud_title\".equals(feature)) ModConfig.HUD_TITLE_ENABLED.set(enabled);\n else if (\"hud_actionbar\".equals(feature)) ModConfig.HUD_ACTIONBAR_ENABLED.set(enabled);\n else if (\"ftb\".equals(feature)) ModConfig.MOD_FTB_QUESTS_ENABLED.set(enabled);\n else if (\"tips\".equals(feature)) ModConfig.MOD_TIPS_ENABLED.set(enabled);\n else if (\"gui\".equals(feature)) ModConfig.CONTENT_GUI_ENABLED.set(enabled);\n else if (\"cache\".equals(feature)) ModConfig.CACHE_ENABLED.set(enabled);\n ModConfig.save();\n }\n\n /** Clears only this mod's Component-JSON cache; configuration is retained. */\n public synchronized void clearCache() {\n translationCache.clear();\n requestQueue.clear();\n TextContextMemory.clear();\n translationCache.save();\n }\n\n /** Clears non-persistent request/session state when the current world ends. */\n public void resetRuntimeState() {\n requestQueue.clear();\n synchronized (this) { TextContextMemory.clear(); }\n TooltipRequestTriggerState.clear();\n }\n\n /** Flushes the categorized baseline cache on disconnect/shutdown. */\n public void flushCache() {\n translationCache.flush();\n if (lineTranslationMemory != null) lineTranslationMemory.flush();\n if (termDictionary != null) termDictionary.save();\n }\n\n public int cancelRequestSurfacePrefix(String surfacePrefix) {\n return requestQueue.cancelSurfacePrefix(surfacePrefix);\n }\n\n public String getRecentRequestErrorStatus() {\n return requestQueue.getRecentErrorStatus();\n }\n\n public void shutdown() {\n flushCache();\n requestQueue.shutdown();\n httpExecutor.shutdownNow();\n }\n\n /** All public translation routes converge here, so a disabled surface never queues HTTP work. */\n public synchronized boolean isSurfaceEnabled(String surface) {\n surface = Surface.normalize(surface);\n if (!ModConfig.GLOBAL_ENABLED.get() || !isFeatureEnabled(\"global\")) return false;\n if (surface.startsWith(\"chat.outgoing\")) return ModConfig.CHAT_OUTGOING_ENABLED.get();\n if (surface.startsWith(\"chat\")) return ModConfig.CHAT_ENABLED.get() && isFeatureEnabled(\"chat\");\n if (surface.startsWith(\"tooltip.visible.item\")) {\n return ModConfig.TOOLTIP_ITEM_ENABLED.get() && isFeatureEnabled(\"tooltip_item\");\n }\n if (surface.startsWith(\"tooltip.visible.chat_hover\")) {\n return ModConfig.TOOLTIP_CHAT_HOVER_ENABLED.get() && isFeatureEnabled(\"tooltip_hover\");\n }\n if (surface.startsWith(\"tooltip.visible.book_hover\")) {\n return ModConfig.TOOLTIP_BOOK_HOVER_ENABLED.get();\n }\n if (\"item_tooltip\".equals(surface) || surface.startsWith(\"tooltip.item\")) {\n return ModConfig.TOOLTIP_ITEM_ENABLED.get() && isFeatureEnabled(\"tooltip_item\");\n }\n if (\"hover_text\".equals(surface) || surface.startsWith(\"hover\")\n || surface.startsWith(\"tooltip.hover\") || surface.startsWith(\"tooltip.chat\")) {\n return ModConfig.TOOLTIP_CHAT_HOVER_ENABLED.get() && isFeatureEnabled(\"tooltip_hover\");\n }\n if (surface.startsWith(\"tooltip.book\")) {\n return ModConfig.TOOLTIP_BOOK_HOVER_ENABLED.get();\n }\n if (surface.startsWith(\"book\")) return ModConfig.CONTENT_BOOK_ENABLED.get() && isFeatureEnabled(\"book\");\n if (surface.startsWith(\"sign\")) return ModConfig.CONTENT_SIGN_ENABLED.get() && isFeatureEnabled(\"sign\");\n if (surface.startsWith(\"advancement\")) return ModConfig.CONTENT_ADVANCEMENT_ENABLED.get() && isFeatureEnabled(\"advancement\");\n if (surface.startsWith(\"entity\")) return ModConfig.CONTENT_ENTITY_NAME_ENABLED.get() && isFeatureEnabled(\"entity_name\");\n if (surface.startsWith(\"scoreboard\") || surface.startsWith(\"player_tab\")\n || \"hud_scoreboard\".equals(surface)) {\n return ModConfig.HUD_SCOREBOARD_ENABLED.get() && isFeatureEnabled(\"hud_scoreboard\");\n }\n if (surface.startsWith(\"bossbar\") || \"hud_bossbar\".equals(surface)) {\n return ModConfig.HUD_BOSSBAR_ENABLED.get() && isFeatureEnabled(\"hud_bossbar\");\n }\n if (surface.startsWith(\"hud.title\") || surface.startsWith(\"hud.subtitle\")\n || surface.startsWith(\"title.\")\n || surface.startsWith(\"hud_title\") || surface.startsWith(\"hud_subtitle\")) {\n return ModConfig.HUD_TITLE_ENABLED.get() && isFeatureEnabled(\"hud_title\");\n }\n if (surface.startsWith(\"hud.actionbar\") || surface.startsWith(\"actionbar.\")\n || surface.startsWith(\"hud_actionbar\")) {\n return ModConfig.HUD_ACTIONBAR_ENABLED.get() && isFeatureEnabled(\"hud_actionbar\");\n }\n if (surface.startsWith(\"hud.captions\")) {\n return (ModConfig.HUD_TITLE_ENABLED.get() || ModConfig.HUD_ACTIONBAR_ENABLED.get())\n && (isFeatureEnabled(\"hud_title\") || isFeatureEnabled(\"hud_actionbar\"));\n }\n if (surface.startsWith(\"ftb\") || surface.startsWith(\"gui.ftb\")) return ModConfig.CONTENT_GUI_ENABLED.get()\n && ModConfig.MOD_TRANSLATION_ENABLED.get()\n && ModConfig.MOD_FTB_QUESTS_ENABLED.get() && isFeatureEnabled(\"ftb\");\n if (surface.startsWith(\"tips\")) return ModConfig.CONTENT_GUI_ENABLED.get()\n && ModConfig.MOD_TRANSLATION_ENABLED.get()\n && ModConfig.MOD_TIPS_ENABLED.get() && isFeatureEnabled(\"tips\");\n if (surface.startsWith(\"gui\")) return ModConfig.CONTENT_GUI_ENABLED.get() && isFeatureEnabled(\"gui\");\n if (\"hud\".equals(surface)) return isFeatureEnabled(\"hud_overlay\");\n return true;\n }\n\n public synchronized void updateConfiguration(String endpoint, String apiKey, String model) {\n updateConfiguration(endpoint, apiKey, model, sourceLanguage, targetLanguage);\n }\n\n public synchronized void updateConfiguration(String endpoint, String apiKey, String model, String targetLanguage) {\n updateConfiguration(endpoint, apiKey, model, sourceLanguage, targetLanguage);\n }\n\n /**\n * Stores both language ends because source-language choice changes a model\n * result just as materially as the destination. Keeping it in the cache\n * identity prevents an explicit English source setting from reusing a\n * previous AUTO translation.\n */\n public synchronized void updateConfiguration(String endpoint, String apiKey, String model,\n String sourceLanguage, String targetLanguage) {\n String updatedEndpoint = normalizeEndpoint(endpoint);\n String updatedApiKey = apiKey == null ? \"\" : apiKey.trim();\n String updatedModel = blankOr(model, DEFAULT_MODEL);\n String updatedSourceLanguage = blankOr(sourceLanguage, \"auto\");\n String updatedTargetLanguage = blankOr(targetLanguage, \"Chinese\");\n boolean requestIdentityChanged = !this.endpoint.equals(updatedEndpoint)\n || !this.apiKey.equals(updatedApiKey)\n || !this.model.equals(updatedModel)\n || !this.sourceLanguage.equals(updatedSourceLanguage)\n || !this.targetLanguage.equals(updatedTargetLanguage);\n this.endpoint = updatedEndpoint;\n this.apiKey = updatedApiKey;\n this.model = updatedModel;\n this.sourceLanguage = updatedSourceLanguage;\n this.targetLanguage = updatedTargetLanguage;\n ModConfig.DEEPSEEK_API_URL.set(updatedEndpoint);\n ModConfig.DEEPSEEK_API_KEY.set(updatedApiKey);\n ModConfig.DEEPSEEK_MODEL.set(updatedModel);\n ModConfig.SOURCE_LANGUAGE.set(updatedSourceLanguage);\n ModConfig.TARGET_LANGUAGE.set(updatedTargetLanguage);\n if (requestIdentityChanged) {\n // Live-saved request settings take effect immediately. Old work\n // must not keep a request lane occupied or publish a response\n // produced with credentials or languages that are no longer active.\n requestQueue.clear();\n SimpleTranslateForge1122.onTranslationSettingsChanged();\n }\n ModConfig.save();\n }\n\n public synchronized void applyModernConfiguration() {\n updateConfiguration(ModConfig.DEEPSEEK_API_URL.get(), ModConfig.DEEPSEEK_API_KEY.get(),\n ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get()),\n ModConfig.SOURCE_LANGUAGE.get(), ModConfig.TARGET_LANGUAGE.get());\n setMaxParallelRequests(ModConfig.API_MAX_PARALLEL_REQUESTS.get());\n }\n\n public synchronized void migrateLegacyConfigurationToModern() {\n ModConfig.DEEPSEEK_API_URL.set(endpoint);\n ModConfig.DEEPSEEK_API_KEY.set(apiKey);\n ModConfig.DEEPSEEK_MODEL.set(model);\n ModConfig.SOURCE_LANGUAGE.set(sourceLanguage);\n ModConfig.TARGET_LANGUAGE.set(targetLanguage);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(maxParallelRequests);\n ModConfig.CHAT_MODE.set(chatAutoMode ? ModConfig.TranslationMode.AUTO : ModConfig.TranslationMode.BUTTON);\n ModConfig.HOLD_ORIGINAL_ENABLED.set(holdOriginalEnabled);\n ModConfig.TOOLTIP_ITEM_TRIGGER_MODE.set(manualTooltipMode\n ? ModConfig.TooltipTriggerMode.SHORTCUT : ModConfig.TooltipTriggerMode.HOVER);\n ModConfig.CHAT_OUTGOING_ENABLED.set(outgoingChatEnabled);\n ModConfig.TOOLTIP_GLOW_ENABLED.set(tooltipGlowEnabled);\n ModConfig.TOKEN_MONITOR_ENABLED.set(tokenMonitorEnabled);\n ModConfig.API_TEXT_CONTEXT_ENABLED.set(textContextEnabled);\n for (Map.Entry entry : legacyTextContextScopeEnabled.entrySet()) {\n applyLegacyContextScope(entry.getKey(), entry.getValue().booleanValue());\n }\n for (Map.Entry entry : legacyFeatureEnabled.entrySet()) {\n applyLegacyFeature(entry.getKey(), entry.getValue().booleanValue());\n }\n ModConfig.save();\n }\n\n /**\n * Sends one deliberately small Component-JSON document through the normal\n * model transport without touching cache, context, or game text. The\n * settings UI uses this to verify the currently persisted credentials.\n */\n public CompletableFuture verifyApiAccess() {\n lastRequestFailure = \"\";\n final String endpointSnapshot;\n final String apiKeySnapshot;\n final String modelSnapshot;\n final String sourceSnapshot;\n final String targetSnapshot;\n synchronized (this) {\n endpointSnapshot = endpoint;\n apiKeySnapshot = apiKey;\n modelSnapshot = model;\n sourceSnapshot = sourceLanguage;\n targetSnapshot = targetLanguage;\n }\n if (apiKeySnapshot.trim().isEmpty()\n && ModConfig.API_FORMAT.get() != ModConfig.ApiFormat.LOCAL_OLLAMA) {\n return CompletableFuture.completedFuture(ApiCheckResult.failed(\"missing_api_key\"));\n }\n if (modelSnapshot.trim().isEmpty()) {\n return CompletableFuture.completedFuture(ApiCheckResult.failed(\"missing_model\"));\n }\n final String requestKey = \"api_check\\u0001\" + sha256(endpointSnapshot + \"\\n\" + apiKeySnapshot + \"\\n\" + modelSnapshot);\n return requestQueue.submit(requestKey, \"api_check\",\n new Supplier>() {\n @Override public CompletableFuture get() {\n return executeHttpAsync(new HttpOperation() {\n @Override public TranslationResult execute(\n CancellableHttpFuture future) {\n JsonArray document = new JsonArray();\n JsonObject component = new JsonObject();\n component.addProperty(\"text\", \"connection check\");\n document.add(component);\n String response = requestComponentDocument(\n document, \"api_check\", sourceSnapshot, targetSnapshot, future);\n return response == null ? TranslationResult.failed(\"request_failed\")\n : TranslationResult.success(response);\n }\n });\n }\n }).thenApply(new Function() {\n @Override public ApiCheckResult apply(TranslationResult result) {\n if (result != null && result.isSuccess()) return ApiCheckResult.available();\n String detail = lastRequestFailure;\n if ((detail == null || detail.trim().isEmpty()) && result != null) {\n detail = result.getFailureReason();\n }\n return ApiCheckResult.failed(detail == null || detail.trim().isEmpty()\n ? \"request_failed\" : detail);\n }\n });\n }\n\n public static final class ApiCheckResult {\n private final boolean available;\n private final String status;\n\n private ApiCheckResult(boolean available, String status) {\n this.available = available;\n this.status = status;\n }\n\n public boolean isAvailable() { return available; }\n public String getStatus() { return status; }\n private static ApiCheckResult available() { return new ApiCheckResult(true, \"available\"); }\n private static ApiCheckResult failed(String status) { return new ApiCheckResult(false, status); }\n }\n\n /** Java-8 provider model discovery used by the complete model settings page. */\n public CompletableFuture detectAvailableModels(\n final String suppliedApiKey, final String suppliedApiUrl, final ModConfig.ApiFormat format) {\n return executeHttpAsync(new HttpOperation() {\n @Override public TranslationDiagnostics.ModelDetection execute(\n CancellableHttpFuture future) {\n String url = modelsEndpoint(suppliedApiUrl, format);\n HttpURLConnection connection = null;\n try {\n connection = (HttpURLConnection) new URL(url).openConnection();\n future.attach(connection);\n connection.setRequestMethod(\"GET\");\n connection.setConnectTimeout(5000);\n connection.setReadTimeout(15000);\n applyDiagnosticAuth(connection, suppliedApiKey, format);\n int status = connection.getResponseCode();\n String body = readResponseBody(connection, status);\n if (status < 200 || status >= 300) {\n return new TranslationDiagnostics.ModelDetection(false, url, status,\n Collections.emptyList(), \"HTTP \" + status);\n }\n JsonElement parsed = new JsonParser().parse(body);\n List models = new ArrayList();\n if (parsed.isJsonObject()) {\n JsonObject object = parsed.getAsJsonObject();\n collectModelIds(object.get(\"data\"), models);\n collectModelIds(object.get(\"models\"), models);\n }\n Collections.sort(models);\n return new TranslationDiagnostics.ModelDetection(!models.isEmpty(), url, status, models,\n models.isEmpty() ? \"No models returned\" : \"available\");\n } catch (Exception error) {\n return new TranslationDiagnostics.ModelDetection(false, url, 0,\n Collections.emptyList(), safeDiagnosticMessage(error));\n } finally {\n future.detach(connection);\n if (connection != null) connection.disconnect();\n }\n }\n });\n }\n\n /** Sends the provider's smallest legal request without changing saved settings. */\n public CompletableFuture verifyModelAccess(\n final String suppliedApiKey, final String suppliedApiUrl, final String suppliedModel,\n final ModConfig.ApiFormat format) {\n return executeHttpAsync(new HttpOperation() {\n @Override public TranslationDiagnostics.ModelAccess execute(\n CancellableHttpFuture future) {\n String modelId = suppliedModel == null ? \"\" : suppliedModel.trim();\n if (modelId.isEmpty()) {\n return new TranslationDiagnostics.ModelAccess(false, \"\", 0, \"Model ID not configured\");\n }\n HttpURLConnection connection = null;\n try {\n String endpoint = diagnosticEndpoint(suppliedApiUrl, format, modelId, suppliedApiKey);\n connection = (HttpURLConnection) new URL(endpoint).openConnection();\n future.attach(connection);\n connection.setRequestMethod(\"POST\");\n connection.setConnectTimeout(5000);\n connection.setReadTimeout(15000);\n connection.setDoOutput(true);\n connection.setRequestProperty(\"Content-Type\", \"application/json\");\n applyDiagnosticAuth(connection, suppliedApiKey, format);\n byte[] body = diagnosticRequest(format, modelId).toString().getBytes(StandardCharsets.UTF_8);\n connection.setFixedLengthStreamingMode(body.length);\n OutputStream output = connection.getOutputStream();\n output.write(body);\n output.close();\n int status = connection.getResponseCode();\n readResponseBody(connection, status);\n boolean success = status >= 200 && status < 300;\n return new TranslationDiagnostics.ModelAccess(success, modelId, status,\n success ? \"available\" : \"HTTP \" + status);\n } catch (Exception error) {\n return new TranslationDiagnostics.ModelAccess(false, modelId, 0,\n safeDiagnosticMessage(error));\n } finally {\n future.detach(connection);\n if (connection != null) connection.disconnect();\n }\n }\n });\n }\n\n /**\n * Returns a validated cached component or the original component. A miss is\n * queued asynchronously; it never performs HTTP work on the client thread.\n */\n public ITextComponent translateCachedOrEnqueue(ITextComponent original, String surface) {\n if (original == null) return null;\n if (HoldOriginalState.isHoldingSurface(surface) || !isSurfaceEnabled(surface) || isBlacklisted(original)) return original;\n DynamicTextTemplate template = dynamicSurface(surface) ? DynamicTextTemplate.capture(original) : null;\n ITextComponent requestSource = template != null && template.hasValues() && template.normalized() != null\n ? template.normalized() : original;\n ComponentTranslationResult result = DirectSurfaceTranslator.translateComponent(\n requestSource, Surface.normalize(surface), \"game-text\");\n if (result == null || result.component == null) return original;\n if (template != null && template.hasValues()) {\n ITextComponent restored = template.restore(result.component);\n return restored == null ? original : restored;\n }\n return result.component;\n }\n\n private static boolean dynamicSurface(String surface) {\n String value = Surface.normalize(surface);\n return value.startsWith(\"hud\") || value.startsWith(\"title\") || value.startsWith(\"actionbar\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar\")\n || value.startsWith(\"player_tab\");\n }\n\n /** Cache-only lookup used by collect-window surfaces before they batch a miss. */\n public ITextComponent getCachedComponent(ITextComponent original, String surface) {\n if (original == null || HoldOriginalState.isHoldingSurface(surface) || !isSurfaceEnabled(surface) || isBlacklisted(original)) return null;\n DynamicTextTemplate template = dynamicSurface(surface) ? DynamicTextTemplate.capture(original) : null;\n ITextComponent requestSource = template != null && template.hasValues() && template.normalized() != null\n ? template.normalized() : original;\n ComponentListTranslationResult result = DirectSurfaceTranslator.getCachedComponents(\n Collections.singletonList(requestSource), Surface.normalize(surface), \"game-text\",\n DirectSurfaceTranslator.isFixedLayoutSurface(surface), \"\");\n if (result == null || !result.translated || result.components == null\n || result.components.size() != 1) return null;\n ITextComponent translated = result.components.get(0);\n if (template != null && template.hasValues()) {\n ITextComponent restored = template.restore(translated);\n return restored == null ? null : restored;\n }\n return translated;\n }\n\n /**\n * Baseline-compatible SPI entry point. Every request item is an exact\n * 1.12.2 Component JSON document; both the wire payload and accepted\n * response are top-level arrays. There is no prose/string fallback.\n */\n @Override\n public CompletableFuture translate(final TranslationRequest request) {\n if (request == null || request.getComponentJson().isEmpty() || !isSurfaceEnabled(request.getSurface())) {\n return CompletableFuture.completedFuture(TranslationResult.failed(\"surface disabled or empty\"));\n }\n if (!isConfigured()) {\n return CompletableFuture.completedFuture(TranslationResult.failed(\"API key is not configured\"));\n }\n String requestKey = sha256(request.getSurface() + \"\\u0001\" + request.getSourceLanguage() + \"\\u0001\"\n + request.getTargetLanguage() + \"\\u0001\" + request.getComponentJson().toString());\n return requestQueue.submit(requestKey, request.getSurface(),\n new Supplier>() {\n @Override public CompletableFuture get() {\n return executeHttpAsync(new HttpOperation() {\n @Override public TranslationResult execute(\n CancellableHttpFuture future) {\n try {\n JsonArray document = new JsonArray();\n JsonArray originalDocument = new JsonArray();\n for (String source : request.getComponentJson()) {\n JsonElement parsed = new JsonParser().parse(source);\n ITextComponent component = ITextComponent.Serializer.jsonToComponent(parsed.toString());\n if (isBlacklisted(component)) return TranslationResult.failed(\"blacklisted source\");\n originalDocument.add(copyJson(parsed));\n document.add(stripHoverEvents(parsed));\n }\n String response = requestComponentDocument(document, request.getSurface(),\n request.getSourceLanguage(), request.getTargetLanguage(), request.terms(),\n request.promptContext(), request.maxTokenMultiplier(), future);\n if (response == null) return TranslationResult.failed(\"invalid response or request failure\");\n JsonArray translated = new JsonParser().parse(response).getAsJsonArray();\n for (int i = 0; i < translated.size(); i++) {\n reattachHoverEvents(originalDocument.get(i), translated.get(i));\n ITextComponent.Serializer.jsonToComponent(translated.get(i).toString());\n }\n return TranslationResult.success(translated.toString());\n } catch (TranslationRequestQueue.RetryableRequestException retryable) {\n throw retryable;\n } catch (Exception ignored) {\n return TranslationResult.failed(\"invalid component JSON\");\n }\n }\n });\n }\n });\n }\n\n /**\n * Low-level provider entry used by the canonical JSON passthrough pipeline.\n * The provider's assistant text is returned verbatim so the pipeline owns\n * exact array/count/Component validation and its bounded structural retry.\n */\n public CompletableFuture translateRawComponentDocument(\n final String componentDocument, final String surface,\n final int maxTokenMultiplier, final String sourceLanguage,\n final String targetLanguage, final String promptMetadata,\n final List termHints) {\n if (componentDocument == null || componentDocument.trim().isEmpty()\n || !isSurfaceEnabled(surface) || !isConfigured()) {\n return CompletableFuture.completedFuture(null);\n }\n final JsonArray document;\n try {\n JsonElement parsed = new JsonParser().parse(componentDocument);\n if (!parsed.isJsonArray()) return CompletableFuture.completedFuture(null);\n document = parsed.getAsJsonArray();\n for (JsonElement component : document) {\n ITextComponent.Serializer.jsonToComponent(component.toString());\n }\n } catch (Exception invalidDocument) {\n return CompletableFuture.completedFuture(null);\n }\n if (document.size() == 0) return CompletableFuture.completedFuture(\"[]\");\n final String requestKey = \"provider_raw\\u0001\" + sha256(surface + \"\\u0001\"\n + sourceLanguage + \"\\u0001\" + targetLanguage + \"\\u0001\"\n + maxTokenMultiplier + \"\\u0001\" + promptMetadata + \"\\u0001\" + componentDocument);\n return requestQueue.submit(requestKey, surface,\n new Supplier>() {\n @Override public CompletableFuture get() {\n return executeHttpAsync(new HttpOperation() {\n @Override public TranslationResult execute(\n CancellableHttpFuture future) {\n String response = requestComponentDocument(document, surface, sourceLanguage, targetLanguage,\n termHints, promptMetadata, maxTokenMultiplier, false, true, future);\n return response == null || response.trim().isEmpty()\n ? TranslationResult.failed(\"blank provider response\")\n : TranslationResult.success(response);\n }\n });\n }\n }).thenApply(new Function() {\n @Override public String apply(TranslationResult result) {\n return result != null && result.isSuccess() ? result.getComponentJsonArray() : null;\n }\n });\n }\n\n /**\n * Adapter for 1.12-only APIs that expose text as a raw String. The request\n * still goes through the Component-JSON protocol; this method never sends\n * or caches a plain-text document.\n */\n public String translateStringCachedOrEnqueue(String original, String surface) {\n if (original == null || original.isEmpty()) return original;\n ITextComponent translated = translateCachedOrEnqueue(new TextComponentString(original), surface);\n return translated == null ? original : translated.getUnformattedText();\n }\n\n /** Dedicated item-tooltip route; it never consumes hidden chat hover text. */\n public List translateTooltipLines(List original) {\n if (original == null || original.isEmpty()) return original;\n if (!isConfigured() || HoldOriginalState.isHoldingSurface(\"item_tooltip\")\n || !isSurfaceEnabled(\"item_tooltip\")) return original;\n final String surface = \"tooltip.visible.item.component.v2\";\n final String role = \"visible-tooltip-component\";\n final String reuseScope = buildTooltipReuseScope(original);\n List translated = new ArrayList(original);\n List visibleComponents = new ArrayList(original.size());\n List missingComponents = new ArrayList();\n List missingSources = new ArrayList();\n List missingIndexes = new ArrayList();\n for (int index = 0; index < original.size(); index++) {\n String line = original.get(index);\n ITextComponent component = new TextComponentString(line == null ? \"\" : line);\n visibleComponents.add(component);\n if (isBlacklisted(component)) return original;\n String remembered = lineTranslationMemory.lookupScoped(line, sourceLanguage, targetLanguage,\n surface, role, reuseScope, ModConfig.API_TEXT_CONTEXT_ALLOW_SHARED.get());\n if (remembered != null) {\n translated.set(index, remembered);\n } else {\n missingComponents.add(component);\n missingSources.add(line == null ? \"\" : line);\n missingIndexes.add(Integer.valueOf(index));\n }\n }\n if (missingComponents.isEmpty()) return translated;\n final String requestSignature = TooltipRequestTriggerState.requestSignature(\n TooltipRequestTriggerState.Context.ITEM, visibleComponents);\n TooltipTranslationGlowRenderer.observe(requestSignature);\n StringBuilder context = new StringBuilder(\"Incremental Component request. Full ordered item tooltip:\");\n for (int i = 0; i < original.size(); i++) context.append(\"\\n\").append(i + 1).append(\": \").append(original.get(i));\n ComponentListTranslationResult result = DirectSurfaceTranslator.getCachedComponents(\n missingComponents, surface, role, false, context.toString());\n if (result == null || !result.translated) {\n if (!TooltipRequestTriggerState.allowRequest(\n TooltipRequestTriggerState.Context.ITEM, visibleComponents)) return original;\n if (TooltipRequestTriggerState.beginRequest(requestSignature)) {\n final List requestComponents = new ArrayList(missingComponents);\n DirectSurfaceTranslator.translateComponentsAsync(\n requestComponents, surface, role, false, context.toString())\n .whenComplete(new java.util.function.BiConsumer() {\n @Override public void accept(ComponentListTranslationResult asyncResult, Throwable error) {\n boolean success = error == null && asyncResult != null && asyncResult.translated\n && asyncResult.components != null\n && asyncResult.components.size() == requestComponents.size();\n TooltipRequestTriggerState.finishRequest(requestSignature, success);\n }\n });\n }\n return original;\n }\n if (result == null || !result.translated || result.components == null\n || result.components.size() != missingComponents.size()) return original;\n List accepted = new ArrayList(result.components.size());\n for (int i = 0; i < result.components.size(); i++) {\n String value = result.components.get(i).getFormattedText();\n accepted.add(value);\n translated.set(missingIndexes.get(i).intValue(), value);\n }\n lineTranslationMemory.recordScoped(missingSources, accepted, sourceLanguage, targetLanguage,\n surface, role, reuseScope, false);\n return translated;\n }\n\n private static String buildTooltipReuseScope(List lines) {\n StringBuilder normalized = new StringBuilder();\n for (String line : lines) {\n String value = line == null ? \"\" : line.replaceAll(\"\\\\u00a7.\", \"\")\n .replaceAll(\"[-+]?\\\\d+(?:[.,:/-]\\\\d+)*\", \"\")\n .replaceAll(\"\\\\s+\", \" \").trim();\n normalized.append(value).append('\\n');\n }\n return TranslationCacheKeys.semanticHash(normalized.toString());\n }\n\n /**\n * Dedicated SHOW_TEXT hover path. It deliberately receives the hover\n * component itself, rather than piggybacking on the visible chat/message\n * request: visible components strip hoverEvent payloads by contract.\n */\n public ITextComponent translateHoverTextCachedOrEnqueue(ITextComponent original) {\n if (original == null) return null;\n if (!isConfigured() || HoldOriginalState.isHoldingSurface(\"tooltip.hover\") || !isSurfaceEnabled(\"hover_text\")\n || isBlacklisted(original)) return original;\n final String surface = \"tooltip.visible.chat_hover.component.v2\";\n final String role = \"hover-block\";\n final List components = Collections.singletonList(original);\n final String requestSignature = TooltipRequestTriggerState.requestSignature(\n TooltipRequestTriggerState.Context.CHAT_HOVER, components);\n TooltipTranslationGlowRenderer.observe(requestSignature);\n ComponentListTranslationResult cached = DirectSurfaceTranslator.getCachedComponents(\n components, surface, role,\n DirectSurfaceTranslator.isFixedLayoutSurface(surface), \"\");\n if (cached != null && cached.translated && cached.components != null && cached.components.size() == 1) {\n return cached.components.get(0);\n }\n if (!TooltipRequestTriggerState.allowRequest(TooltipRequestTriggerState.Context.CHAT_HOVER,\n Collections.singletonList(original))) return original;\n if (TooltipRequestTriggerState.beginRequest(requestSignature)) {\n DirectSurfaceTranslator.translateComponentsAsync(components, surface, role, false, \"\")\n .whenComplete(new java.util.function.BiConsumer() {\n @Override public void accept(ComponentListTranslationResult asyncResult, Throwable error) {\n TooltipRequestTriggerState.finishRequest(requestSignature,\n error == null && asyncResult != null && asyncResult.translated\n && asyncResult.components != null && asyncResult.components.size() == 1);\n }\n });\n }\n return original;\n }\n\n public ITextComponent translateBookHoverTextCachedOrEnqueue(ITextComponent original) {\n if (original == null || !isConfigured() || HoldOriginalState.isHolding(HoldOriginalFeature.TOOLTIP_HOVER)\n || !isSurfaceEnabled(\"tooltip.book\") || isBlacklisted(original)) return original;\n final String surface = \"tooltip.visible.book_hover.component.v2\";\n final String role = \"book-hover-block\";\n final List components = Collections.singletonList(original);\n final String requestSignature = \"BOOK:\" + TooltipRequestTriggerState.requestSignature(\n TooltipRequestTriggerState.Context.CHAT_HOVER, components);\n TooltipTranslationGlowRenderer.observe(requestSignature);\n ComponentListTranslationResult cached = DirectSurfaceTranslator.getCachedComponents(\n components, surface, role, false, \"\");\n if (cached != null && cached.translated && cached.components != null && cached.components.size() == 1) {\n TooltipRequestTriggerState.finishRequest(requestSignature, true);\n return cached.components.get(0);\n }\n if (TooltipRequestTriggerState.beginRequest(requestSignature)) {\n DirectSurfaceTranslator.translateComponentsAsync(components, surface, role, false, \"\")\n .whenComplete(new java.util.function.BiConsumer() {\n @Override public void accept(ComponentListTranslationResult asyncResult, Throwable error) {\n TooltipRequestTriggerState.finishRequest(requestSignature,\n error == null && asyncResult != null && asyncResult.translated\n && asyncResult.components != null && asyncResult.components.size() == 1);\n }\n });\n }\n return original;\n }\n\n /** Book NBT pages can be either plain strings or serialized ITextComponents. */\n public String translateBookPageCachedOrEnqueue(String serializedPage) {\n if (serializedPage == null || serializedPage.isEmpty()) return serializedPage;\n try {\n ITextComponent source = ITextComponent.Serializer.jsonToComponent(serializedPage);\n ITextComponent translated = translateCachedOrEnqueue(source, \"book\");\n return translated == source ? serializedPage : ITextComponent.Serializer.componentToJson(translated);\n } catch (Exception ignored) {\n return translateStringCachedOrEnqueue(serializedPage, \"book\");\n }\n }\n\n private String requestComponentDocument(JsonArray document, String surface,\n String sourceLanguage, String targetLanguage,\n CancellableHttpFuture requestFuture)\n throws TranslationRequestQueue.RetryableRequestException {\n return requestComponentDocument(document, surface, sourceLanguage, targetLanguage,\n termDictionary.matchTermsInText(document == null ? \"\" : document.toString()), \"\", 1,\n requestFuture);\n }\n\n private String requestComponentDocument(JsonArray document, String surface,\n String sourceLanguage, String targetLanguage,\n List termHints,\n String explicitPromptContext, int maxTokenMultiplier,\n CancellableHttpFuture requestFuture)\n throws TranslationRequestQueue.RetryableRequestException {\n return requestComponentDocument(document, surface, sourceLanguage, targetLanguage,\n termHints, explicitPromptContext, maxTokenMultiplier, true, false, requestFuture);\n }\n\n private String requestComponentDocument(JsonArray document, String surface,\n String sourceLanguage, String targetLanguage,\n List termHints,\n String explicitPromptContext, int maxTokenMultiplier,\n boolean validateComponentResponse,\n boolean promptContextIsMetadata,\n CancellableHttpFuture requestFuture)\n throws TranslationRequestQueue.RetryableRequestException {\n String endpointSnapshot;\n String apiKeySnapshot;\n String modelSnapshot;\n String languageSnapshot;\n boolean contextEnabledSnapshot;\n boolean tokenMonitorSnapshot;\n ModConfig.ApiFormat apiFormatSnapshot = ModConfig.API_FORMAT.get();\n synchronized (this) {\n endpointSnapshot = endpoint;\n apiKeySnapshot = apiKey;\n modelSnapshot = model;\n languageSnapshot = targetLanguage;\n contextEnabledSnapshot = ModConfig.API_TEXT_CONTEXT_ENABLED.get();\n tokenMonitorSnapshot = ModConfig.TOKEN_MONITOR_ENABLED.get();\n }\n try {\n long requestStartedAt = System.currentTimeMillis();\n String destination = targetLanguage == null || targetLanguage.trim().isEmpty()\n ? languageSnapshot : targetLanguage.trim();\n String source = sourceLanguage == null || sourceLanguage.trim().isEmpty()\n ? \"auto-detected language\" : sourceLanguage.trim();\n String callerContext = explicitPromptContext == null ? \"\" : explicitPromptContext.trim();\n String promptContext;\n if (promptContextIsMetadata && !callerContext.isEmpty()) {\n promptContext = callerContext;\n } else {\n TextContextMemory.PromptMetadata metadata = TextContextMemory.buildPromptMetadata(\n callerContext, surface, \"game-text\", document.toString(),\n contextEnabledSnapshot && isTextContextAllowedForSurface(surface), source, destination);\n promptContext = metadata.json();\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n source, destination, termHints, surface, promptContext);\n int maxTokens = Math.max(512, Math.min(32768,\n document.toString().length() * Math.max(1, maxTokenMultiplier) * 2));\n JsonObject request = buildTranslationProviderRequest(apiFormatSnapshot, endpointSnapshot,\n modelSnapshot, systemPrompt, document.toString(), maxTokens);\n\n String requestEndpoint = diagnosticEndpoint(endpointSnapshot, apiFormatSnapshot,\n modelSnapshot, apiKeySnapshot);\n HttpURLConnection connection = (HttpURLConnection) new URL(requestEndpoint).openConnection();\n try {\n if (requestFuture != null) requestFuture.attach(connection);\n connection.setRequestMethod(\"POST\");\n connection.setConnectTimeout(30000);\n connection.setReadTimeout(60000);\n connection.setDoOutput(true);\n connection.setRequestProperty(\"Content-Type\", \"application/json\");\n applyDiagnosticAuth(connection, apiKeySnapshot, apiFormatSnapshot);\n byte[] body = request.toString().getBytes(StandardCharsets.UTF_8);\n connection.setFixedLengthStreamingMode(body.length);\n OutputStream output = connection.getOutputStream();\n output.write(body);\n output.close();\n int responseCode = connection.getResponseCode();\n if (responseCode < 200 || responseCode >= 300) {\n String bodyText = readResponseBody(connection, responseCode);\n String safeBody = bodyText;\n if (apiKeySnapshot != null && !apiKeySnapshot.isEmpty()) {\n safeBody = safeBody.replace(apiKeySnapshot, \"***\");\n }\n SimpleTranslateForge1122.getLogger().warn(\n \"Translation provider rejected surface={} status={}{}\",\n surface, responseCode, compactDiagnosticBody(safeBody));\n if (\"api_check\".equals(surface)) {\n lastRequestFailure = \"HTTP \" + responseCode + compactDiagnosticBody(safeBody);\n }\n if (isRetryableHttpStatus(responseCode)) {\n throw new TranslationRequestQueue.RetryableRequestException(\"HTTP \" + responseCode);\n }\n return null;\n }\n BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8));\n StringBuilder response = new StringBuilder();\n String line;\n while ((line = reader.readLine()) != null) response.append(line);\n reader.close();\n try {\n JsonObject responseJson = new JsonParser().parse(response.toString()).getAsJsonObject();\n String content = extractAssistantContent(responseJson);\n if (content == null || content.trim().isEmpty()) {\n SimpleTranslateForge1122.getLogger().warn(\n \"Translation provider returned no assistant text surface={} response={}\",\n surface, compactDiagnosticBody(response.toString()));\n if (\"api_check\".equals(surface)) {\n lastRequestFailure = \"provider response did not contain assistant text\";\n }\n throw new TranslationRequestQueue.RetryableRequestException(\n \"provider response did not contain assistant text\");\n }\n content = content.trim();\n if (tokenMonitorSnapshot) {\n int[] usage = extractTokenUsage(responseJson);\n int promptTokens = usage[0];\n int completionTokens = usage[1];\n int totalTokens = usage[2];\n if (totalTokens > 0) {\n TokenUsageMonitor.record(new TokenUsage(modelSnapshot, promptTokens, completionTokens, totalTokens,\n System.currentTimeMillis() - requestStartedAt, System.currentTimeMillis(), surface));\n }\n }\n if (!validateComponentResponse) return content;\n JsonElement translatedDocument = new JsonParser().parse(content);\n if (!translatedDocument.isJsonArray() || translatedDocument.getAsJsonArray().size() != document.size()) {\n SimpleTranslateForge1122.getLogger().warn(\n \"Translation provider returned invalid Component array surface={} expected={} response={}\",\n surface, document.size(), compactDiagnosticBody(content));\n if (\"api_check\".equals(surface)) {\n lastRequestFailure = \"provider returned an invalid component array\";\n }\n throw new TranslationRequestQueue.RetryableRequestException(\"provider returned an invalid component array\");\n }\n for (JsonElement translated : translatedDocument.getAsJsonArray()) {\n ITextComponent.Serializer.jsonToComponent(translated.toString());\n }\n if (\"api_check\".equals(surface)) lastRequestFailure = \"\";\n return translatedDocument.toString();\n } catch (TranslationRequestQueue.RetryableRequestException retryable) {\n throw retryable;\n } catch (Exception invalidResponse) {\n SimpleTranslateForge1122.getLogger().warn(\n \"Translation provider returned invalid JSON surface={} reason={} response={}\",\n surface, safeDiagnosticMessage(invalidResponse), compactDiagnosticBody(response.toString()));\n throw new TranslationRequestQueue.RetryableRequestException(\"provider returned invalid component JSON\");\n }\n } finally {\n if (requestFuture != null) requestFuture.detach(connection);\n connection.disconnect();\n }\n } catch (TranslationRequestQueue.RetryableRequestException retryable) {\n throw retryable;\n } catch (java.io.IOException error) {\n SimpleTranslateForge1122.getLogger().warn(\"Translation request I/O failure surface={} reason={}\",\n surface, safeDiagnosticMessage(error));\n if (\"api_check\".equals(surface)) lastRequestFailure = safeDiagnosticMessage(error);\n throw new TranslationRequestQueue.RetryableRequestException(\"I/O error\");\n } catch (Exception error) {\n SimpleTranslateForge1122.getLogger().warn(\"Translation request failed surface={} reason={}\",\n surface, safeDiagnosticMessage(error));\n if (\"api_check\".equals(surface)) lastRequestFailure = safeDiagnosticMessage(error);\n return null;\n }\n }\n\n private static boolean isRetryableHttpStatus(int statusCode) {\n return statusCode == 408 || statusCode == 425 || statusCode == 429\n || statusCode == 500 || statusCode == 502 || statusCode == 503 || statusCode == 504;\n }\n\n private static JsonElement stripHoverEvents(JsonElement value) {\n if (value == null || value.isJsonNull() || value.isJsonPrimitive()) return copyJson(value);\n if (value.isJsonArray()) {\n JsonArray copy = new JsonArray();\n for (JsonElement child : value.getAsJsonArray()) copy.add(stripHoverEvents(child));\n return copy;\n }\n JsonObject copy = new JsonObject();\n for (Map.Entry entry : value.getAsJsonObject().entrySet()) {\n if (!\"hoverEvent\".equals(entry.getKey()) && !\"hover_event\".equals(entry.getKey())) {\n copy.add(entry.getKey(), stripHoverEvents(entry.getValue()));\n }\n }\n return copy;\n }\n\n private static void reattachHoverEvents(JsonElement original, JsonElement translated) {\n if (original == null || translated == null || !original.isJsonObject() || !translated.isJsonObject()) return;\n JsonObject source = original.getAsJsonObject();\n JsonObject target = translated.getAsJsonObject();\n if (source.has(\"hoverEvent\")) target.add(\"hoverEvent\", copyJson(source.get(\"hoverEvent\")));\n if (source.has(\"hover_event\")) target.add(\"hover_event\", copyJson(source.get(\"hover_event\")));\n reattachChildren(source.get(\"extra\"), target.get(\"extra\"));\n }\n\n private static void reattachChildren(JsonElement original, JsonElement translated) {\n if (original == null || translated == null || !original.isJsonArray() || !translated.isJsonArray()) return;\n int count = Math.min(original.getAsJsonArray().size(), translated.getAsJsonArray().size());\n for (int i = 0; i < count; i++) reattachHoverEvents(original.getAsJsonArray().get(i), translated.getAsJsonArray().get(i));\n }\n\n /** Gson 2.8.0 bundled with 1.12.2 has no public JsonElement#deepCopy. */\n private static JsonElement copyJson(JsonElement value) {\n if (value == null) return null;\n if (value.isJsonNull() || value.isJsonPrimitive()) return value;\n if (value.isJsonArray()) {\n JsonArray copy = new JsonArray();\n for (JsonElement child : value.getAsJsonArray()) copy.add(copyJson(child));\n return copy;\n }\n JsonObject copy = new JsonObject();\n for (Map.Entry entry : value.getAsJsonObject().entrySet()) {\n copy.add(entry.getKey(), copyJson(entry.getValue()));\n }\n return copy;\n }\n\n private static String normalizeContextScope(String scope) {\n if (scope == null || scope.trim().isEmpty()) return \"other\";\n String normalized = scope.trim().toLowerCase(java.util.Locale.ROOT);\n for (String candidate : TEXT_CONTEXT_SCOPES) {\n if (candidate.equals(normalized)) return candidate;\n }\n return \"other\";\n }\n\n private static String contextScopeForSurface(String surface) {\n String normalized = Surface.normalize(surface);\n if (normalized.startsWith(\"chat.outgoing\")) return \"sent_chat\";\n if (normalized.startsWith(\"chat\")) return \"received_chat\";\n if (\"item_tooltip\".equals(normalized) || normalized.startsWith(\"tooltip.item\")) return \"item_tooltip\";\n if (normalized.startsWith(\"tooltip.visible.book_hover\") || normalized.startsWith(\"tooltip.book\")) return \"book\";\n if (\"hover_text\".equals(normalized) || normalized.startsWith(\"hover\")\n || normalized.startsWith(\"tooltip.hover\") || normalized.startsWith(\"tooltip.visible.chat_hover\")) {\n return \"hover_tooltip\";\n }\n if (normalized.startsWith(\"book\")) return \"book\";\n if (normalized.startsWith(\"sign\")) return \"sign\";\n if (normalized.startsWith(\"advancement\")) return \"hud_progress\";\n if (normalized.startsWith(\"entity\")) return \"entity_name\";\n if (normalized.startsWith(\"hud.title\") || normalized.startsWith(\"hud.subtitle\")\n || normalized.startsWith(\"hud.actionbar\") || normalized.startsWith(\"hud.captions\")\n || normalized.startsWith(\"title.\") || normalized.startsWith(\"actionbar.\")) return \"hud_captions\";\n if (normalized.startsWith(\"scoreboard\") || normalized.startsWith(\"bossbar\")\n || normalized.startsWith(\"player_tab\")) return \"hud_progress\";\n return \"other\";\n }\n\n private static String sha256(String text) {\n try {\n byte[] digest = MessageDigest.getInstance(\"SHA-256\").digest(text.getBytes(StandardCharsets.UTF_8));\n StringBuilder output = new StringBuilder(digest.length * 2);\n for (byte value : digest) output.append(String.format(\"%02x\", value & 0xff));\n return output.toString();\n } catch (Exception ignored) {\n return Integer.toHexString(text.hashCode());\n }\n }\n\n private synchronized void load() {\n if (!configFile.exists()) return;\n Properties properties = new Properties();\n try {\n InputStream input = new FileInputStream(configFile);\n properties.load(input);\n input.close();\n String persistedEndpoint = blankOr(properties.getProperty(\"endpoint\"), endpoint);\n endpoint = normalizeEndpoint(persistedEndpoint);\n apiKey = properties.getProperty(\"apiKey\", apiKey);\n model = blankOr(properties.getProperty(\"model\"), model);\n sourceLanguage = blankOr(properties.getProperty(\"sourceLanguage\"), sourceLanguage);\n targetLanguage = blankOr(properties.getProperty(\"targetLanguage\"), targetLanguage);\n maxParallelRequests = clampWorkers(parseInt(properties.getProperty(\"maxParallelRequests\"), maxParallelRequests));\n chatAutoMode = Boolean.parseBoolean(properties.getProperty(\"chatAutoMode\", \"false\"));\n holdOriginalEnabled = Boolean.parseBoolean(properties.getProperty(\"holdOriginalEnabled\", \"false\"));\n manualTooltipMode = Boolean.parseBoolean(properties.getProperty(\"manualTooltipMode\", \"false\"));\n outgoingChatEnabled = Boolean.parseBoolean(properties.getProperty(\"outgoingChatEnabled\", \"false\"));\n tooltipGlowEnabled = Boolean.parseBoolean(properties.getProperty(\"tooltipGlowEnabled\", \"false\"));\n tokenMonitorEnabled = Boolean.parseBoolean(properties.getProperty(\"tokenMonitorEnabled\", \"false\"));\n textContextEnabled = Boolean.parseBoolean(properties.getProperty(\"textContextEnabled\", \"true\"));\n textContextMessageCount = Math.max(0, Math.min(20, parseInt(properties.getProperty(\"textContextMessageCount\"), 6)));\n for (String scope : TEXT_CONTEXT_SCOPES) {\n String persisted = properties.getProperty(\"textContext.\" + scope);\n if (persisted != null) legacyTextContextScopeEnabled.put(scope,\n Boolean.valueOf(Boolean.parseBoolean(persisted)));\n }\n String[] legacyFeatures = {\"global\", \"chat\", \"tooltip_item\", \"tooltip_hover\", \"book\", \"sign\",\n \"advancement\", \"entity_name\", \"hud_scoreboard\", \"hud_bossbar\", \"hud_title\",\n \"hud_actionbar\", \"ftb\", \"tips\", \"gui\", \"cache\"};\n for (String feature : legacyFeatures) {\n String persisted = properties.getProperty(\"feature.\" + feature);\n if (persisted != null) legacyFeatureEnabled.put(feature,\n Boolean.valueOf(Boolean.parseBoolean(persisted)));\n }\n } catch (Exception ignored) {\n }\n }\n\n private static int integer(JsonObject object, String key) {\n try {\n return object.has(key) ? object.get(key).getAsInt() : 0;\n } catch (Exception ignored) {\n return 0;\n }\n }\n\n private static int[] extractTokenUsage(JsonObject response) {\n int prompt = 0, completion = 0, total = 0;\n if (response != null && response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n JsonObject usage = response.getAsJsonObject(\"usage\");\n prompt = Math.max(integer(usage, \"prompt_tokens\"), integer(usage, \"input_tokens\"));\n completion = Math.max(integer(usage, \"completion_tokens\"), integer(usage, \"output_tokens\"));\n total = integer(usage, \"total_tokens\");\n }\n if (response != null && response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n JsonObject usage = response.getAsJsonObject(\"usageMetadata\");\n prompt = Math.max(prompt, integer(usage, \"promptTokenCount\"));\n completion = Math.max(completion, integer(usage, \"candidatesTokenCount\"));\n total = Math.max(total, integer(usage, \"totalTokenCount\"));\n }\n if (response != null) {\n prompt = Math.max(prompt, integer(response, \"prompt_eval_count\"));\n completion = Math.max(completion, integer(response, \"eval_count\"));\n }\n if (total == 0) total = prompt + completion;\n return new int[]{prompt, completion, total};\n }\n\n private static boolean isShareableComponentEntry(String key, String value) {\n if (key == null || value == null || !TranslationCacheKeys.isCurrentProtocolKey(key)\n || !key.contains(\":fmt=\" + COMPONENT_JSON_FORMAT + \":\")) return false;\n try {\n ITextComponent.Serializer.jsonToComponent(value);\n return true;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static String modelsEndpoint(String rawUrl, ModConfig.ApiFormat format) {\n String endpoint = blankOr(rawUrl, DEFAULT_ENDPOINT).replaceAll(\"/+$\", \"\");\n ModConfig.ApiFormat resolved = format == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : format;\n if (resolved == ModConfig.ApiFormat.LOCAL_OLLAMA) {\n int api = endpoint.indexOf(\"/api/\");\n return (api >= 0 ? endpoint.substring(0, api) : endpoint) + \"/api/tags\";\n }\n if (resolved == ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT) {\n int models = endpoint.indexOf(\"/models/\");\n String base = models >= 0 ? endpoint.substring(0, models) : endpoint;\n if (!base.endsWith(\"/models\")) base += \"/models\";\n return base;\n }\n if (endpoint.endsWith(\"/chat/completions\")) {\n return endpoint.substring(0, endpoint.length() - \"/chat/completions\".length()) + \"/models\";\n }\n if (endpoint.endsWith(\"/responses\")) {\n return endpoint.substring(0, endpoint.length() - \"/responses\".length()) + \"/models\";\n }\n if (endpoint.endsWith(\"/messages\")) {\n return endpoint.substring(0, endpoint.length() - \"/messages\".length()) + \"/models\";\n }\n return endpoint.endsWith(\"/models\") ? endpoint : endpoint + \"/models\";\n }\n\n private static String diagnosticEndpoint(String rawUrl, ModConfig.ApiFormat format,\n String modelId, String suppliedApiKey) {\n String endpoint = blankOr(rawUrl, DEFAULT_ENDPOINT).replaceAll(\"/+$\", \"\");\n ModConfig.ApiFormat resolved = format == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : format;\n if (resolved == ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT) {\n endpoint = endpoint.replace(\"{model}\", modelId);\n if (!endpoint.contains(\":generateContent\")) {\n int models = endpoint.indexOf(\"/models/\");\n String base = models >= 0 ? endpoint.substring(0, models) : endpoint;\n endpoint = base + \"/models/\" + modelId + \":generateContent\";\n }\n if (suppliedApiKey != null && !suppliedApiKey.trim().isEmpty() && !endpoint.contains(\"key=\")) {\n endpoint += (endpoint.contains(\"?\") ? \"&\" : \"?\") + \"key=\"\n + urlEncode(suppliedApiKey.trim());\n }\n return endpoint;\n }\n if (resolved == ModConfig.ApiFormat.LOCAL_OLLAMA) {\n if (endpoint.contains(\"/chat/completions\")) return endpoint;\n return endpoint + \"/chat/completions\";\n }\n if (resolved == ModConfig.ApiFormat.ANTHROPIC_MESSAGES) {\n if (endpoint.endsWith(\"/messages\")) return endpoint;\n return endpoint.endsWith(\"/v1\") ? endpoint + \"/messages\" : endpoint + \"/v1/messages\";\n }\n if (resolved == ModConfig.ApiFormat.OPENAI_RESPONSES) {\n return endpoint.endsWith(\"/responses\") ? endpoint : endpoint + \"/responses\";\n }\n String normalized = normalizeEndpoint(endpoint);\n return normalized.contains(\"/chat/completions\") ? normalized : normalized + \"/chat/completions\";\n }\n\n private static JsonObject diagnosticRequest(ModConfig.ApiFormat format, String modelId) {\n ModConfig.ApiFormat resolved = format == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : format;\n JsonObject request = new JsonObject();\n request.addProperty(\"model\", modelId);\n if (resolved == ModConfig.ApiFormat.OPENAI_RESPONSES) {\n request.addProperty(\"input\", \"Return exactly this JSON array: [{\\\"text\\\":\\\"ok\\\"}]\");\n request.addProperty(\"max_output_tokens\", 32);\n return request;\n }\n if (resolved == ModConfig.ApiFormat.ANTHROPIC_MESSAGES) {\n request.addProperty(\"max_tokens\", 32);\n }\n if (resolved == ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT) {\n request.remove(\"model\");\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", \"Return exactly this JSON array: [{\\\"text\\\":\\\"ok\\\"}]\");\n JsonArray parts = new JsonArray();\n parts.add(part);\n JsonObject content = new JsonObject();\n content.add(\"parts\", parts);\n JsonArray contents = new JsonArray();\n contents.add(content);\n request.add(\"contents\", contents);\n return request;\n }\n request.addProperty(\"stream\", false);\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n message.addProperty(\"content\", \"Return exactly this JSON array: [{\\\"text\\\":\\\"ok\\\"}]\");\n JsonArray messages = new JsonArray();\n messages.add(message);\n request.add(\"messages\", messages);\n if (resolved != ModConfig.ApiFormat.LOCAL_OLLAMA) request.addProperty(\"max_tokens\", 32);\n return request;\n }\n\n private static JsonObject buildTranslationProviderRequest(ModConfig.ApiFormat format, String endpointBase,\n String modelId,\n String systemPrompt, String userPrompt,\n int maxTokens) {\n ModConfig.ApiFormat resolved = format == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : format;\n JsonObject request = new JsonObject();\n request.addProperty(\"model\", modelId);\n int outputTokens = Math.max(512, Math.min(32768, maxTokens));\n boolean openAiReasoning = isOpenAiReasoningModel(modelId);\n if (resolved == ModConfig.ApiFormat.OPENAI_RESPONSES) {\n request.addProperty(\"stream\", false);\n if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\", openAiReasoningEffort(modelId,\n ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n request.add(\"reasoning\", reasoning);\n } else {\n request.addProperty(\"temperature\", 0.0D);\n }\n request.addProperty(\"max_output_tokens\", Math.min(32768, outputTokens));\n request.addProperty(\"instructions\", systemPrompt);\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n JsonArray content = new JsonArray();\n content.add(part);\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n message.add(\"content\", content);\n JsonArray input = new JsonArray();\n input.add(message);\n request.add(\"input\", input);\n return request;\n }\n if (resolved == ModConfig.ApiFormat.ANTHROPIC_MESSAGES) {\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n request.add(\"thinking\", thinking);\n } else {\n request.addProperty(\"temperature\", 0.0D);\n }\n request.addProperty(\"max_tokens\", Math.min(32768, outputTokens));\n request.addProperty(\"system\", systemPrompt);\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n message.addProperty(\"content\", userPrompt);\n JsonArray messages = new JsonArray();\n messages.add(message);\n request.add(\"messages\", messages);\n return request;\n }\n if (resolved == ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT) {\n request.remove(\"model\");\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n JsonArray systemParts = new JsonArray();\n systemParts.add(systemPart);\n JsonObject systemInstruction = new JsonObject();\n systemInstruction.add(\"parts\", systemParts);\n request.add(\"systemInstruction\", systemInstruction);\n JsonObject userPart = new JsonObject();\n userPart.addProperty(\"text\", userPrompt);\n JsonArray userParts = new JsonArray();\n userParts.add(userPart);\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n content.add(\"parts\", userParts);\n JsonArray contents = new JsonArray();\n contents.add(content);\n request.add(\"contents\", contents);\n JsonObject generation = new JsonObject();\n generation.addProperty(\"temperature\", 0.0D);\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) outputTokens = Math.max(outputTokens, 2048);\n generation.addProperty(\"maxOutputTokens\", Math.min(32768, outputTokens));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generation.add(\"thinkingConfig\", thinkingConfig);\n } else if (modelId != null && modelId.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generation.add(\"thinkingConfig\", thinkingConfig);\n }\n request.add(\"generationConfig\", generation);\n return request;\n }\n request.addProperty(\"stream\", false);\n boolean deepSeekThinkingCapable = resolved == ModConfig.ApiFormat.DEEPSEEK_CHAT\n || ((resolved == ModConfig.ApiFormat.OPENAI_CHAT_COMPAT\n || resolved == ModConfig.ApiFormat.LOCAL_OLLAMA)\n && (isOfficialDeepSeekEndpoint(endpointBase) || isDeepSeekReasoningModel(modelId)));\n boolean thinkingEnabled = deepSeekThinkingCapable && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n openAiReasoning = !deepSeekThinkingCapable && openAiReasoning;\n if (thinkingEnabled) outputTokens = Math.max(outputTokens, 4096);\n else if (openAiReasoning) outputTokens = Math.max(outputTokens, 2048);\n request.addProperty(\"max_tokens\", Math.min(32768, outputTokens));\n if (!thinkingEnabled && !openAiReasoning) request.addProperty(\"temperature\", 0.0D);\n if (deepSeekThinkingCapable) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n request.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n request.addProperty(\"reasoning_effort\", openAiReasoningEffort(modelId,\n ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n JsonArray messages = new JsonArray();\n JsonObject system = new JsonObject();\n system.addProperty(\"role\", \"system\");\n system.addProperty(\"content\", systemPrompt);\n messages.add(system);\n JsonObject user = new JsonObject();\n user.addProperty(\"role\", \"user\");\n user.addProperty(\"content\", userPrompt);\n messages.add(user);\n request.add(\"messages\", messages);\n return request;\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.trim().isEmpty()) return false;\n try {\n String host = new URL(apiUrl).getHost();\n if (host == null) return false;\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return \"deepseek.com\".equals(normalized) || normalized.endsWith(\".deepseek.com\");\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.trim().isEmpty()) return false;\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\") || normalized.contains(\"r1\") || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.trim().isEmpty()) return false;\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\") || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\") || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) return \"medium\";\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private static String extractAssistantContent(JsonObject response) {\n if (response == null) return null;\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()\n && response.getAsJsonArray(\"choices\").size() > 0) {\n JsonObject choice = response.getAsJsonArray(\"choices\").get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonElement content = choice.getAsJsonObject(\"message\").get(\"content\");\n if (content != null && !content.isJsonNull()) return content.getAsString();\n }\n }\n if (response.has(\"message\") && response.get(\"message\").isJsonObject()) {\n JsonElement content = response.getAsJsonObject(\"message\").get(\"content\");\n if (content != null && !content.isJsonNull()) return content.getAsString();\n }\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n for (JsonElement output : response.getAsJsonArray(\"output\")) {\n if (!output.isJsonObject()) continue;\n JsonElement content = output.getAsJsonObject().get(\"content\");\n if (content == null || !content.isJsonArray()) continue;\n for (JsonElement part : content.getAsJsonArray()) {\n if (part.isJsonObject() && part.getAsJsonObject().has(\"text\")) {\n return part.getAsJsonObject().get(\"text\").getAsString();\n }\n }\n }\n }\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n for (JsonElement part : response.getAsJsonArray(\"content\")) {\n if (part.isJsonObject() && part.getAsJsonObject().has(\"text\")) {\n return part.getAsJsonObject().get(\"text\").getAsString();\n }\n }\n }\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()\n && response.getAsJsonArray(\"candidates\").size() > 0) {\n JsonObject candidate = response.getAsJsonArray(\"candidates\").get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonElement parts = candidate.getAsJsonObject(\"content\").get(\"parts\");\n if (parts != null && parts.isJsonArray()) {\n StringBuilder text = new StringBuilder();\n for (JsonElement part : parts.getAsJsonArray()) {\n if (part.isJsonObject() && part.getAsJsonObject().has(\"text\")) {\n text.append(part.getAsJsonObject().get(\"text\").getAsString());\n }\n }\n if (text.length() > 0) return text.toString();\n }\n }\n }\n return null;\n }\n\n private static void applyDiagnosticAuth(HttpURLConnection connection, String suppliedApiKey,\n ModConfig.ApiFormat format) {\n String key = suppliedApiKey == null ? \"\" : suppliedApiKey.trim();\n ModConfig.ApiFormat resolved = format == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : format;\n if (key.isEmpty() || resolved == ModConfig.ApiFormat.LOCAL_OLLAMA\n || resolved == ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT) return;\n if (resolved == ModConfig.ApiFormat.ANTHROPIC_MESSAGES) {\n connection.setRequestProperty(\"x-api-key\", key);\n connection.setRequestProperty(\"anthropic-version\", \"2023-06-01\");\n } else {\n connection.setRequestProperty(\"Authorization\", \"Bearer \" + key);\n }\n }\n\n private static void collectModelIds(JsonElement element, List target) {\n if (element == null || !element.isJsonArray()) return;\n for (JsonElement item : element.getAsJsonArray()) {\n String id = \"\";\n if (item.isJsonPrimitive()) id = item.getAsString();\n else if (item.isJsonObject()) {\n JsonObject object = item.getAsJsonObject();\n for (String key : new String[]{\"id\", \"name\", \"model\"}) {\n if (object.has(key) && object.get(key).isJsonPrimitive()) {\n id = object.get(key).getAsString();\n break;\n }\n }\n }\n if (id.startsWith(\"models/\")) id = id.substring(\"models/\".length());\n if (!id.trim().isEmpty() && !target.contains(id)) target.add(id);\n }\n }\n\n private static String readResponseBody(HttpURLConnection connection, int status) throws java.io.IOException {\n InputStream input = status >= 200 && status < 400\n ? connection.getInputStream() : connection.getErrorStream();\n if (input == null) return \"\";\n BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));\n try {\n StringBuilder body = new StringBuilder();\n String line;\n while ((line = reader.readLine()) != null) body.append(line);\n return body.toString();\n } finally {\n reader.close();\n }\n }\n\n private static String compactDiagnosticBody(String body) {\n if (body == null || body.trim().isEmpty()) return \"\";\n String compact = body.replace('\\r', ' ').replace('\\n', ' ').replaceAll(\"\\\\s+\", \" \").trim();\n if (compact.length() > 180) compact = compact.substring(0, 180) + \"...\";\n return \": \" + compact;\n }\n\n private static String safeDiagnosticMessage(Exception error) {\n if (error == null) return \"request_failed\";\n String message = error.getMessage();\n return message == null || message.trim().isEmpty()\n ? error.getClass().getSimpleName() : message;\n }\n\n private static String urlEncode(String value) {\n try { return java.net.URLEncoder.encode(value, \"UTF-8\"); }\n catch (Exception ignored) { return value; }\n }\n\n private static String blankOr(String value, String fallback) {\n return value == null || value.trim().isEmpty() ? fallback : value.trim();\n }\n\n /** Accept DeepSeek's documented base URL while this legacy UI stores a full POST endpoint. */\n private static String normalizeEndpoint(String value) {\n String endpoint = blankOr(value, DEFAULT_ENDPOINT);\n try {\n java.net.URL url = new java.net.URL(endpoint);\n if (\"api.deepseek.com\".equalsIgnoreCase(url.getHost())\n && (url.getQuery() == null || url.getQuery().isEmpty())) {\n String path = url.getPath() == null ? \"\" : url.getPath();\n if (path.isEmpty() || \"/\".equals(path) || \"/v1\".equals(path) || \"/v1/\".equals(path)\n || (\"/chat/completions\".startsWith(path) && path.startsWith(\"/chat/\"))) {\n return url.getProtocol() + \"://\" + url.getAuthority() + \"/chat/completions\";\n }\n }\n } catch (Exception ignored) {\n // Preserve custom/provider endpoints verbatim and let API detection report them.\n }\n return endpoint;\n }\n\n private CompletableFuture executeHttpAsync(final HttpOperation operation) {\n final CancellableHttpFuture result = new CancellableHttpFuture();\n Future worker = httpExecutor.submit(new Runnable() {\n @Override public void run() {\n if (result.isCancelled()) return;\n try {\n T value = operation.execute(result);\n if (!result.isCancelled()) result.complete(value);\n } catch (CancellationException canceled) {\n result.cancel(true);\n } catch (Throwable error) {\n if (!result.isCancelled()) result.completeExceptionally(error);\n }\n }\n });\n result.setWorker(worker);\n return result;\n }\n\n private interface HttpOperation {\n T execute(CancellableHttpFuture future) throws Exception;\n }\n\n /** CompletableFuture whose cancellation interrupts and disconnects the actual legacy HTTP call. */\n private static final class CancellableHttpFuture extends CompletableFuture {\n private final AtomicReference connection =\n new AtomicReference();\n private volatile Future worker;\n\n private void setWorker(Future value) {\n worker = value;\n if (isCancelled() && value != null) value.cancel(true);\n }\n\n private void attach(HttpURLConnection value) {\n if (value == null) return;\n connection.set(value);\n if (isCancelled()) {\n connection.compareAndSet(value, null);\n value.disconnect();\n throw new CancellationException(\"HTTP request canceled\");\n }\n }\n\n private void detach(HttpURLConnection value) {\n if (value != null) connection.compareAndSet(value, null);\n }\n\n @Override public boolean cancel(boolean mayInterruptIfRunning) {\n HttpURLConnection active = connection.getAndSet(null);\n if (active != null) active.disconnect();\n Future activeWorker = worker;\n if (activeWorker != null) activeWorker.cancel(mayInterruptIfRunning);\n return super.cancel(mayInterruptIfRunning);\n }\n }\n\n private void applyRequestWorkerCount() {\n requestQueue.setMaxParallelRequests(clampWorkers(maxParallelRequests));\n }\n\n private static void applyLegacyContextScope(String scope, boolean enabled) {\n if (\"received_chat\".equals(scope)) ModConfig.API_TEXT_CONTEXT_RECEIVED_CHAT.set(enabled);\n else if (\"sent_chat\".equals(scope)) ModConfig.API_TEXT_CONTEXT_SENT_CHAT.set(enabled);\n else if (\"item_tooltip\".equals(scope)) ModConfig.API_TEXT_CONTEXT_ITEM_TOOLTIP.set(enabled);\n else if (\"hover_tooltip\".equals(scope)) ModConfig.API_TEXT_CONTEXT_HOVER_TOOLTIP.set(enabled);\n else if (\"book\".equals(scope)) ModConfig.API_TEXT_CONTEXT_BOOK.set(enabled);\n else if (\"sign\".equals(scope)) ModConfig.API_TEXT_CONTEXT_SIGN.set(enabled);\n else if (\"hud_captions\".equals(scope)) ModConfig.API_TEXT_CONTEXT_HUD_CAPTIONS.set(enabled);\n else if (\"hud_progress\".equals(scope)) ModConfig.API_TEXT_CONTEXT_HUD_PROGRESS.set(enabled);\n else if (\"entity_name\".equals(scope)) ModConfig.API_TEXT_CONTEXT_ENTITY_NAME.set(enabled);\n }\n\n private static void applyLegacyFeature(String feature, boolean enabled) {\n if (\"global\".equals(feature)) ModConfig.GLOBAL_ENABLED.set(enabled);\n else if (\"chat\".equals(feature)) ModConfig.CHAT_ENABLED.set(enabled);\n else if (\"tooltip_item\".equals(feature)) ModConfig.TOOLTIP_ITEM_ENABLED.set(enabled);\n else if (\"tooltip_hover\".equals(feature)) ModConfig.TOOLTIP_CHAT_HOVER_ENABLED.set(enabled);\n else if (\"book\".equals(feature)) ModConfig.CONTENT_BOOK_ENABLED.set(enabled);\n else if (\"sign\".equals(feature)) ModConfig.CONTENT_SIGN_ENABLED.set(enabled);\n else if (\"advancement\".equals(feature)) ModConfig.CONTENT_ADVANCEMENT_ENABLED.set(enabled);\n else if (\"entity_name\".equals(feature)) ModConfig.CONTENT_ENTITY_NAME_ENABLED.set(enabled);\n else if (\"hud_scoreboard\".equals(feature)) ModConfig.HUD_SCOREBOARD_ENABLED.set(enabled);\n else if (\"hud_bossbar\".equals(feature)) ModConfig.HUD_BOSSBAR_ENABLED.set(enabled);\n else if (\"hud_title\".equals(feature)) ModConfig.HUD_TITLE_ENABLED.set(enabled);\n else if (\"hud_actionbar\".equals(feature)) ModConfig.HUD_ACTIONBAR_ENABLED.set(enabled);\n else if (\"ftb\".equals(feature)) ModConfig.MOD_FTB_QUESTS_ENABLED.set(enabled);\n else if (\"tips\".equals(feature)) ModConfig.MOD_TIPS_ENABLED.set(enabled);\n else if (\"gui\".equals(feature)) ModConfig.CONTENT_GUI_ENABLED.set(enabled);\n else if (\"cache\".equals(feature)) ModConfig.CACHE_ENABLED.set(enabled);\n }\n\n private static int clampWorkers(int workers) {\n return Math.max(MIN_REQUEST_WORKERS, Math.min(MAX_REQUEST_WORKERS, workers));\n }\n\n private static int parseInt(String raw, int fallback) {\n try { return Integer.parseInt(raw); } catch (Exception ignored) { return fallback; }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-Forge-1.18.2/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745703.4180493, "n": 32006, "h": "b3cf756d44702d28", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.10/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745703.5455875, "n": 32296, "h": "aad5b9f90664476b", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /**\n * When true, multi-region PUA-positioned HUD trees (including recognized\n * Wynncraft selector actionbars) keep the original. Default off: ordinary\n * layout HUDs retain their existing in-place path, while recognized Wynn\n * selectors use their dedicated source-anchored semantic renderer.\n */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_WYNN_OVERLAY_ENABLED = bool(\"hud.wynnOverlayEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755569.3837605, "n": 22912, "h": "55128dae66f63229", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", true);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755571.7078578, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.10/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745703.6248767, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745703.98684, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-26.2/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745704.228985, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.7/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745704.4027748, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755588.0235097, "n": 61591, "h": "6e9645a768c95133", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n private volatile ApiRequestProfile lastSuccessfulProfile;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = ModConfig.SOURCE_LANGUAGE.get();\n String targetLanguage = ModConfig.TARGET_LANGUAGE.get();\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface());\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n /** Output budget follows the Component JSON array, not the optional context preface. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int contextEnd = body.indexOf(\"[/CONTEXT]\");\n if (contextEnd >= 0) {\n body = body.substring(contextEnd + \"[/CONTEXT]\".length()).trim();\n }\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n SimpleTranslateMod.getLogger().debug(\n \"Translation API returned no assistant text; skipping identical automatic retry\");\n return null;\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n if (chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n && \"response.output_text.delta\".equals(chunk.get(\"type\").getAsString())\n && chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n lastSuccessfulProfile = profile;\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase();\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.9/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745704.4407427, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745704.655264, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755583.4805555, "n": 61591, "h": "6e9645a768c95133", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n private volatile ApiRequestProfile lastSuccessfulProfile;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = ModConfig.SOURCE_LANGUAGE.get();\n String targetLanguage = ModConfig.TARGET_LANGUAGE.get();\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface());\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n /** Output budget follows the Component JSON array, not the optional context preface. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int contextEnd = body.indexOf(\"[/CONTEXT]\");\n if (contextEnd >= 0) {\n body = body.substring(contextEnd + \"[/CONTEXT]\".length()).trim();\n }\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n SimpleTranslateMod.getLogger().debug(\n \"Translation API returned no assistant text; skipping identical automatic retry\");\n return null;\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n if (chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n && \"response.output_text.delta\".equals(chunk.get(\"type\").getAsString())\n && chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n lastSuccessfulProfile = profile;\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase();\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.2/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745706.1720672, "n": 13765, "h": "94b333841d2e5bb4", "c": "package com.yourname.simpletranslate.gui;\n\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.config.ModConfig.ApiFormat;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport net.minecraft.client.Minecraft;\nimport net.minecraft.client.gui.components.Button;\nimport net.minecraft.client.gui.components.CycleButton;\nimport net.minecraft.client.gui.components.EditBox;\nimport net.minecraft.client.gui.screens.Screen;\nimport net.minecraft.network.chat.Component;\nimport net.minecraft.network.chat.Style;\nimport net.minecraft.util.FormattedCharSequence;\n\n/**\n * Compact model/API settings screen.\n */\npublic class ModelSettingsScreen extends ScrollableSettingsScreen {\n\n private String currentApiUrl;\n private String currentApiKey;\n private String currentModelId;\n private ApiFormat currentApiFormat;\n private boolean currentThinkingEnabled;\n private int currentMaxParallelRequests;\n private String status = \"\";\n private boolean checking;\n\n private EditBox apiUrlInput;\n private EditBox apiKeyInput;\n private EditBox modelInput;\n private CycleButton formatButton;\n private CycleButton thinkingButton;\n private CycleButton maxParallelButton;\n private Button checkButton;\n\n public ModelSettingsScreen(Screen parent) {\n super(Component.translatable(\"screen.simple_translate.model_settings\"), parent);\n this.currentApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n this.currentApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n this.currentModelId = ModConfig.DEEPSEEK_MODEL.get();\n this.currentApiFormat = ModConfig.API_FORMAT.get();\n this.currentThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n this.currentMaxParallelRequests = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n this.contentWidth = 300;\n }\n\n @Override\n protected void buildContent() {\n addSectionHeader(text(\"screen.simple_translate.model_settings.section.connection\"));\n\n this.apiUrlInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_url_hint\"));\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.api_key_hint\"));\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? FormattedCharSequence.EMPTY\n : FormattedCharSequence.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new EditBox(this.font, 0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setHint(Component.translatable(\"screen.simple_translate.model_settings.model_hint\"));\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> Component.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.api_format\"),\n (button, value) -> {\n this.currentApiFormat = value;\n if (this.modelInput != null && (this.modelInput.getValue() == null || this.modelInput.getValue().isBlank())) {\n this.modelInput.setValue(value.getDefaultModel());\n }\n if (value == ApiFormat.LOCAL_OLLAMA) {\n if (this.apiUrlInput != null && (this.apiUrlInput.getValue() == null\n || this.apiUrlInput.getValue().isBlank()\n || ModConfig.DEFAULT_API_URL.equals(this.apiUrlInput.getValue()))) {\n this.apiUrlInput.setValue(\"http://localhost:11434/v1\");\n }\n if (this.apiKeyInput != null && (this.apiKeyInput.getValue() == null || this.apiKeyInput.getValue().isBlank())) {\n this.apiKeyInput.setValue(\"ollama\");\n }\n }\n });\n withTooltip(this.formatButton, \"screen.simple_translate.model_settings.api_format.tooltip\");\n addEntry(this.formatButton);\n addDescription(text(\"screen.simple_translate.model_settings.local_llm_warning\"));\n addEntry(this.formatButton);\n\n this.thinkingButton = CycleButton.onOffBuilder(this.currentThinkingEnabled)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.thinking\"),\n (button, value) -> {\n this.currentThinkingEnabled = value;\n applyLiveSettings();\n });\n withTooltip(this.thinkingButton, \"screen.simple_translate.thinking.tooltip\");\n addEntry(this.thinkingButton);\n\n this.maxParallelButton = CycleButton.builder(value -> Component.literal(String.valueOf(value))).withInitialValue(this.currentMaxParallelRequests)\n .withValues(1, 2, 3, 4, 5, 6, 7, 8)\n .create(0, 0, this.contentWidth, 20,\n Component.translatable(\"screen.simple_translate.model_settings.max_parallel\"),\n (button, value) -> this.currentMaxParallelRequests = value);\n withTooltip(this.maxParallelButton, \"screen.simple_translate.model_settings.max_parallel.tooltip\");\n addEntry(this.maxParallelButton);\n\n Component checkLabel = this.checking\n ? Component.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check\")\n : Component.literal(shortText(this.status)));\n this.checkButton = Button.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? Component.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : Component.literal(this.status));\n addEntry(this.checkButton);\n }\n\n @Override\n protected void repositionEntries() {\n super.repositionEntries();\n boolean enabled = !this.checking;\n if (this.apiUrlInput != null) {\n this.apiUrlInput.active = this.apiUrlInput.visible && enabled;\n }\n if (this.apiKeyInput != null) {\n this.apiKeyInput.active = this.apiKeyInput.visible && enabled;\n }\n if (this.modelInput != null) {\n this.modelInput.active = this.modelInput.visible && enabled;\n }\n if (this.formatButton != null) {\n this.formatButton.active = this.formatButton.visible && enabled;\n }\n if (this.maxParallelButton != null) {\n this.maxParallelButton.active = this.maxParallelButton.visible && enabled;\n }\n if (this.thinkingButton != null) {\n this.thinkingButton.active = this.thinkingButton.visible && enabled;\n }\n if (this.checkButton != null) {\n this.checkButton.active = this.checkButton.visible && enabled;\n }\n }\n\n private void checkApi() {\n if (this.checking) {\n return;\n }\n captureInputs();\n if (this.currentApiKey.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_key\");\n rebuildScreen();\n return;\n }\n if (this.currentModelId.isBlank()) {\n this.status = text(\"screen.simple_translate.model_settings.status.no_model\");\n rebuildScreen();\n return;\n }\n if (SimpleTranslateMod.getTranslationManager() == null) {\n this.status = text(\"screen.simple_translate.model_settings.status.manager_missing\");\n rebuildScreen();\n return;\n }\n\n this.checking = true;\n this.status = text(\"screen.simple_translate.model_settings.checking\");\n rebuildScreen();\n SimpleTranslateMod.getTranslationManager()\n .verifyModelAccess(this.currentApiKey, this.currentApiUrl, this.currentModelId, this.currentApiFormat)\n .thenAccept(result -> Minecraft.getInstance().execute(() -> finishCheck(result)))\n .exceptionally(error -> {\n Minecraft.getInstance().execute(() -> {\n this.checking = false;\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(error == null ? \"unknown\" : error.getMessage()));\n rebuildScreen();\n });\n return null;\n });\n }\n\n private void finishCheck(TranslationDiagnostics.ModelAccess result) {\n captureInputs();\n this.checking = false;\n if (result != null && result.success()) {\n this.status = text(\"screen.simple_translate.model_settings.status.available\");\n } else {\n this.status = text(\"screen.simple_translate.model_settings.status.failed\",\n shortText(result == null ? \"unknown\" : result.message()));\n }\n rebuildScreen();\n }\n\n private void rebuildScreen() {\n this.clearWidgets();\n this.init();\n }\n\n private void captureInputs() {\n if (this.apiUrlInput != null) {\n this.currentApiUrl = this.apiUrlInput.getValue() == null ? \"\" : this.apiUrlInput.getValue().trim();\n }\n if (this.apiKeyInput != null) {\n this.currentApiKey = this.apiKeyInput.getValue() == null ? \"\" : this.apiKeyInput.getValue().trim();\n }\n if (this.modelInput != null) {\n this.currentModelId = this.modelInput.getValue() == null ? \"\" : this.modelInput.getValue().trim();\n }\n }\n\n @Override\n protected void saveSettings() {\n captureInputs();\n String oldApiKey = ModConfig.DEEPSEEK_API_KEY.get();\n String oldApiUrl = ModConfig.DEEPSEEK_API_URL.get();\n ApiFormat oldApiFormat = ModConfig.API_FORMAT.get();\n int oldMaxParallel = ModConfig.API_MAX_PARALLEL_REQUESTS.get();\n String oldModel = ModConfig.DEEPSEEK_MODEL.get();\n boolean oldThinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n\n String normalizedApiUrl = ModConfig.normalizeApiUrl(this.currentApiUrl);\n String normalizedModel = this.currentApiFormat == ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(this.currentModelId)\n : (this.currentModelId == null || this.currentModelId.isBlank()\n ? this.currentApiFormat.getDefaultModel()\n : this.currentModelId);\n ModConfig.DEEPSEEK_API_KEY.set(this.currentApiKey);\n ModConfig.DEEPSEEK_API_URL.set(normalizedApiUrl);\n ModConfig.API_FORMAT.set(this.currentApiFormat);\n ModConfig.API_MAX_PARALLEL_REQUESTS.set(this.currentMaxParallelRequests);\n ModConfig.DEEPSEEK_MODEL.set(normalizedModel);\n ModConfig.DEEPSEEK_THINKING_ENABLED.set(this.currentThinkingEnabled);\n boolean changed = !oldApiKey.equals(this.currentApiKey)\n || !oldApiUrl.equals(normalizedApiUrl)\n || oldApiFormat != this.currentApiFormat\n || oldMaxParallel != this.currentMaxParallelRequests\n || oldThinkingEnabled != this.currentThinkingEnabled\n || !oldModel.equals(normalizedModel);\n if (changed) {\n SimpleTranslateMod.getLogger().info(\"Model settings changed format={} model={} maxParallel={}\",\n this.currentApiFormat, ModConfig.DEEPSEEK_MODEL.get(), this.currentMaxParallelRequests);\n }\n }\n\n private String text(String key, Object... args) {\n return Component.translatable(key, args).getString();\n }\n\n private String shortText(String text) {\n if (text == null || text.isBlank()) {\n return \"unknown\";\n }\n String compact = text.replace('\\n', ' ').replace('\\r', ' ').trim();\n if (this.currentApiKey != null && !this.currentApiKey.isBlank()) {\n compact = compact.replace(this.currentApiKey, \"***\");\n }\n return compact.length() > 64 ? compact.substring(0, 61) + \"...\" : compact;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.20.5/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745706.583075, "n": 32006, "h": "b3cf756d44702d28", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /** When true, multi-region PUA-positioned HUD trees keep their original layout. */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"hud.wynnOverlayEnabled\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.8/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745706.724769, "n": 32296, "h": "aad5b9f90664476b", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.core.AtomicFiles;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\nimport com.yourname.simpletranslate.keybind.KeyChord;\nimport com.yourname.simpletranslate.keybind.ShortcutAction;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n public static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n /** One-shot migration value consumed while the vanilla key mapping is registered. */\n private static KeyChord legacyOpenSettingsChord = KeyChord.NONE;\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n /** Scope-local smart context applies to every model-backed translation surface. */\n public static final BooleanValue API_TEXT_CONTEXT_ENABLED = bool(\"api.textContext.enabled\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ALLOW_SHARED =\n bool(\"api.textContext.allowSharedImported\", false);\n public static final BooleanValue API_TEXT_CONTEXT_RECEIVED_CHAT =\n bool(\"api.textContext.receivedChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SENT_CHAT =\n bool(\"api.textContext.sentChat\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ITEM_TOOLTIP =\n bool(\"api.textContext.itemTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HOVER_TOOLTIP =\n bool(\"api.textContext.hoverTooltip\", true);\n public static final BooleanValue API_TEXT_CONTEXT_BOOK = bool(\"api.textContext.book\", true);\n public static final BooleanValue API_TEXT_CONTEXT_SIGN = bool(\"api.textContext.sign\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_CAPTIONS =\n bool(\"api.textContext.hudCaptions\", true);\n public static final BooleanValue API_TEXT_CONTEXT_HUD_PROGRESS =\n bool(\"api.textContext.hudProgress\", true);\n public static final BooleanValue API_TEXT_CONTEXT_ENTITY_NAME =\n bool(\"api.textContext.entityName\", true);\n public static final BooleanValue API_TEXT_CONTEXT_TEXT_DISPLAY =\n bool(\"api.textContext.textDisplay\", true);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CONTENT_GUI_ENABLED = bool(\"content.guiEnabled\", true);\n public static final EnumValue CONTENT_GUI_MODE =\n enumValue(\"content.guiMode\", GuiTranslationMode.SHORTCUT, GuiTranslationMode.class);\n /** Remembers that K has opted the in-world HUD/overlay frame into translation. */\n public static final BooleanValue CONTENT_HUD_FRAME_ACTIVE =\n bool(\"content.hudFrameActive\", false);\n /** Remembers which Screens K has opted into whole-frame translation. */\n public static final ConfigValue CONTENT_GUI_FRAME_SCREEN_KEYS =\n stringValue(\"content.guiFrameScreenKeys\", \"\");\n public static final BooleanValue MOD_TRANSLATION_ENABLED = bool(\"mods.enabled\", true);\n public static final BooleanValue MOD_FTB_QUESTS_ENABLED = bool(\"mods.ftbQuestsEnabled\", true);\n public static final BooleanValue MOD_TIPS_ENABLED = bool(\"mods.tipsEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n /**\n * When true, multi-region PUA-positioned HUD trees (including recognized\n * Wynncraft selector actionbars) keep the original. Default off: ordinary\n * layout HUDs retain their existing in-place path, while recognized Wynn\n * selectors use their dedicated source-anchored semantic renderer.\n */\n public static final BooleanValue LAYOUT_CRITICAL_HUD_KEEP_ORIGINAL =\n bool(\"general.layoutCriticalHudKeepOriginal\", false);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final BooleanValue CHAT_OUTGOING_ENABLED = bool(\"chat.outgoingEnabled\", false);\n public static final ConfigValue CHAT_OUTGOING_SERVER_LANGUAGE = stringValue(\"chat.outgoingServerLanguage\", \"en\");\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", true);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", true);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", false);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", true);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", true);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", true);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", true);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", true);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", true);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", true);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", true);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", true);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", true);\n public static final BooleanValue HUD_WYNN_OVERLAY_ENABLED = bool(\"hud.wynnOverlayEnabled\", true);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", false);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final ConfigValue SHORTCUT_TOGGLE_GLOBAL_TRANSLATION = stringValue(\n \"shortcuts.toggleGlobalTranslation\", KeyChord.NONE.serialize());\n public static final ConfigValue SHORTCUT_TOGGLE_CHAT_MODE = stringValue(\"shortcuts.toggleChatMode\", \"keyboard:-1:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_GUI = stringValue(\"shortcuts.translateGui\", \"keyboard:75:0\");\n public static final ConfigValue SHORTCUT_TRANSLATE_TOOLTIP = stringValue(\"shortcuts.translateTooltip\", \"keyboard:86:0\");\n public static final ConfigValue SHORTCUT_SIGN_SELECT = stringValue(\"shortcuts.signSelect\", \"keyboard:71:0\");\n public static final ConfigValue SHORTCUT_SIGN_SUBMIT = stringValue(\"shortcuts.signSubmit\", \"keyboard:72:0\");\n\n public static final ConfigValue HOLD_ORIGINAL_CHORD_CHAT = stringValue(\"holdOriginal.chord.chat\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM = stringValue(\"holdOriginal.chord.tooltipItem\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER = stringValue(\"holdOriginal.chord.tooltipHover\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOOK = stringValue(\"holdOriginal.chord.book\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SIGN = stringValue(\"holdOriginal.chord.sign\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ADVANCEMENT = stringValue(\"holdOriginal.chord.advancement\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ENTITY_NAME = stringValue(\"holdOriginal.chord.entityName\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TEXT_DISPLAY = stringValue(\"holdOriginal.chord.textDisplay\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_SCOREBOARD = stringValue(\"holdOriginal.chord.scoreboard\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_BOSSBAR = stringValue(\"holdOriginal.chord.bossbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_TITLE = stringValue(\"holdOriginal.chord.title\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_ACTIONBAR = stringValue(\"holdOriginal.chord.actionbar\", KeyChord.NONE.serialize());\n public static final ConfigValue HOLD_ORIGINAL_CHORD_GUI = stringValue(\"holdOriginal.chord.gui\", KeyChord.NONE.serialize());\n\n public static ConfigValue getShortcutChord(ShortcutAction action) {\n return switch (action) {\n case TOGGLE_GLOBAL_TRANSLATION -> SHORTCUT_TOGGLE_GLOBAL_TRANSLATION;\n case TOGGLE_CHAT_MODE -> SHORTCUT_TOGGLE_CHAT_MODE;\n case TRANSLATE_GUI -> SHORTCUT_TRANSLATE_GUI;\n case TRANSLATE_TOOLTIP -> SHORTCUT_TRANSLATE_TOOLTIP;\n case SIGN_SELECT -> SHORTCUT_SIGN_SELECT;\n case SIGN_SUBMIT -> SHORTCUT_SIGN_SUBMIT;\n };\n }\n\n public static ConfigValue getHoldOriginalChord(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_CHORD_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_CHORD_BOOK;\n case SIGN -> HOLD_ORIGINAL_CHORD_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_CHORD_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_CHORD_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_CHORD_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_CHORD_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_CHORD_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_CHORD_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_CHORD_ACTIONBAR;\n case GUI -> HOLD_ORIGINAL_CHORD_GUI;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (com.google.gson.JsonParseException | IllegalStateException e) {\n SimpleTranslateMod.getLogger().error(\"Config file is not valid JSON; backing it up before regenerating\", e);\n backupCorruptConfigFile();\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n /** A corrupt config is preserved next to the original instead of being overwritten with defaults. */\n private static void backupCorruptConfigFile() {\n try {\n if (configFile != null && Files.exists(configFile)) {\n Path backup = configFile.resolveSibling(configFile.getFileName() + \".corrupt.bak\");\n Files.move(configFile, backup, java.nio.file.StandardCopyOption.REPLACE_EXISTING);\n SimpleTranslateMod.getLogger().warn(\"Backed up unreadable config to {}\", backup);\n }\n } catch (Exception ignored) {\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n AtomicFiles.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n captureLegacyOpenSettingsChord(root);\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n root.remove(\"chat.contextBatchIntervalMs\");\n root.remove(\"chat.contextCollectWindowMs\");\n root.remove(\"cache.serverShareEnabled\");\n root.remove(\"general.wynncraftProfileMode\");\n root.remove(\"api.routerEnabled\");\n root.remove(\"api.routerEndpoints\");\n migrateLegacyHoldOriginalKeys(root);\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n /**\n * Opening the settings screen is now a normal Minecraft control. Preserve a\n * previous custom binding for this startup only, then remove the obsolete\n * private shortcut value from the mod configuration.\n */\n private static void captureLegacyOpenSettingsChord(JsonObject root) {\n String legacyKey = \"shortcuts.openSettings\";\n if (root != null && root.has(legacyKey) && root.get(legacyKey).isJsonPrimitive()) {\n legacyOpenSettingsChord = KeyChord.parse(root.get(legacyKey).getAsString(), KeyChord.NONE);\n root.remove(legacyKey);\n }\n }\n\n public static synchronized KeyChord consumeLegacyOpenSettingsChord() {\n KeyChord result = legacyOpenSettingsChord;\n legacyOpenSettingsChord = KeyChord.NONE;\n return result;\n }\n\n public static synchronized boolean hasPersistedKey(String key) {\n return loadedRoot != null && key != null && loadedRoot.has(key);\n }\n\n private static void migrateLegacyHoldOriginalKeys(JsonObject root) {\n migrateLegacySingleKey(root, \"holdOriginal.key.chat\", HOLD_ORIGINAL_CHORD_CHAT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipItem\", HOLD_ORIGINAL_CHORD_TOOLTIP_ITEM.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.tooltipHover\", HOLD_ORIGINAL_CHORD_TOOLTIP_HOVER.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.book\", HOLD_ORIGINAL_CHORD_BOOK.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.sign\", HOLD_ORIGINAL_CHORD_SIGN.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.advancement\", HOLD_ORIGINAL_CHORD_ADVANCEMENT.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.entityName\", HOLD_ORIGINAL_CHORD_ENTITY_NAME.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.textDisplay\", HOLD_ORIGINAL_CHORD_TEXT_DISPLAY.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.scoreboard\", HOLD_ORIGINAL_CHORD_SCOREBOARD.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.bossbar\", HOLD_ORIGINAL_CHORD_BOSSBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.title\", HOLD_ORIGINAL_CHORD_TITLE.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.actionbar\", HOLD_ORIGINAL_CHORD_ACTIONBAR.getKey());\n migrateLegacySingleKey(root, \"holdOriginal.key.gui\", HOLD_ORIGINAL_CHORD_GUI.getKey());\n }\n\n private static void migrateLegacySingleKey(JsonObject root, String oldKey, String newKey) {\n if (!root.has(newKey) && root.has(oldKey) && root.get(oldKey).isJsonPrimitive()) {\n try {\n root.addProperty(newKey, KeyChord.keyboard(root.get(oldKey).getAsInt()).serialize());\n } catch (RuntimeException ignored) {\n // Invalid legacy values fall back to the unbound chord.\n }\n }\n root.remove(oldKey);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum GuiTranslationMode {\n SHORTCUT,\n AUTO\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\"),\n LOCAL_OLLAMA(\"本地大模型 Ollama\", \"qwen2.5:7b\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755578.7402744, "n": 22912, "h": "55128dae66f63229", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final IntValue API_MAX_IN_FLIGHT_BATCHES = intValue(\"api.maxInFlightBatches\", 2, 1, 2);\n public static final IntValue API_DIRECT_BATCH_DELAY_MS = intValue(\"api.directBatchDelayMs\", 50, 0, 200);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CUSTOM_FONT_CJK_FIX_ENABLED = bool(\"general.customFontCjkFixEnabled\", true);\n public static final BooleanValue TOKEN_MONITOR_ENABLED = bool(\"monitor.tokenEnabled\", false);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final EnumValue TOOLTIP_ITEM_TRIGGER_MODE =\n enumValue(\"tooltip.itemTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final EnumValue TOOLTIP_CHAT_HOVER_TRIGGER_MODE =\n enumValue(\"tooltip.chatHoverTriggerMode\", TooltipTriggerMode.HOVER, TooltipTriggerMode.class);\n public static final BooleanValue TOOLTIP_GLOW_ENABLED = bool(\"tooltip.glow.enabled\", true);\n public static final IntValue TOOLTIP_GLOW_LINE_WIDTH = intValue(\"tooltip.glow.lineWidth\", 3, 1, 6);\n public static final IntValue TOOLTIP_GLOW_SPREAD = intValue(\"tooltip.glow.spread\", 6, 0, 12);\n public static final IntValue TOOLTIP_GLOW_CYCLE_MS = intValue(\"tooltip.glow.cycleMs\", 8000, 2000, 24000);\n public static final IntValue TOOLTIP_GLOW_OPACITY = intValue(\"tooltip.glow.opacity\", 180, 20, 255);\n public static final EnumValue TOOLTIP_GLOW_THEME =\n enumValue(\"tooltip.glow.theme\", TooltipGlowTheme.SOFT, TooltipGlowTheme.class);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 800, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n root.remove(\"chat.jsonPassthrough\");\n root.remove(\"tooltip.itemJsonPassthrough\");\n root.remove(\"tooltip.chatHoverJsonPassthrough\");\n root.remove(\"hud.stylePrompt\");\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum TooltipTriggerMode {\n HOVER,\n SHORTCUT\n }\n\n public enum TooltipGlowTheme {\n SOFT,\n OCEAN,\n AURORA,\n SUNSET\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}, "3abf57f2e9db35106679cdb12f4b5dfbedac3bb0": {"t": 1785755580.2603633, "n": 22429, "h": "9ea148f3522da882", "c": "package com.yourname.simpletranslate.config;\n\nimport com.google.gson.Gson;\nimport com.google.gson.GsonBuilder;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonNull;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.google.gson.JsonPrimitive;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.keybind.HoldOriginalFeature;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.LinkedHashMap;\nimport java.util.Locale;\nimport java.util.Map;\nimport java.util.function.BiFunction;\nimport java.util.function.Function;\n\npublic class ModConfig {\n private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();\n private static final Map> VALUES = new LinkedHashMap<>();\n private static final String DEFAULT_API_URL = \"https://api.deepseek.com/chat/completions\";\n private static Path configFile;\n private static JsonObject loadedRoot = new JsonObject();\n\n public static final ConfigValue DEEPSEEK_API_KEY = stringValue(\"api.apiKey\", \"\");\n public static final ConfigValue DEEPSEEK_MODEL = stringValue(\"api.model\", DeepSeekModelPreset.V4_FLASH.getModelId());\n public static final BooleanValue DEEPSEEK_THINKING_ENABLED = bool(\"api.thinkingEnabled\", false);\n public static final ConfigValue DEEPSEEK_API_URL = stringValue(\"api.apiUrl\", DEFAULT_API_URL);\n public static final EnumValue API_FORMAT = enumValue(\"api.format\", ApiFormat.DEEPSEEK_CHAT, ApiFormat.class);\n public static final IntValue API_MAX_PARALLEL_REQUESTS = intValue(\"api.maxParallelRequests\", 5, 1, 8);\n public static final ConfigValue SOURCE_LANGUAGE = stringValue(\"language.sourceLanguage\", \"auto\");\n public static final ConfigValue TARGET_LANGUAGE = stringValue(\"language.targetLanguage\", \"zh_cn\");\n public static final BooleanValue GLOBAL_ENABLED = bool(\"general.globalEnabled\", true);\n public static final BooleanValue CHAT_ENABLED = bool(\"chat.enabled\", true);\n public static final EnumValue CHAT_MODE = enumValue(\"chat.mode\", TranslationMode.BUTTON, TranslationMode.class);\n public static final BooleanValue CHAT_CONTEXT_ENABLED = bool(\"chat.contextEnabled\", false);\n public static final IntValue CHAT_CONTEXT_MESSAGE_COUNT = intValue(\"chat.contextMessageCount\", 6, 0, 20);\n public static final IntValue CHAT_CONTEXT_BATCH_INTERVAL_MS = intValue(\"chat.contextBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue CHAT_CONTEXT_COLLECT_WINDOW_MS = intValue(\"chat.contextCollectWindowMs\", 4500, 500, 30000);\n public static final BooleanValue TOOLTIP_ITEM_ENABLED = bool(\"tooltip.itemEnabled\", false);\n public static final BooleanValue TOOLTIP_CHAT_HOVER_ENABLED = bool(\"tooltip.chatHoverEnabled\", false);\n public static final BooleanValue TOOLTIP_BOOK_HOVER_ENABLED = bool(\"tooltip.bookHoverEnabled\", false);\n public static final BooleanValue CONTENT_BOOK_ENABLED = bool(\"content.bookEnabled\", false);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_X = intValue(\"content.bookBookmarkOffsetX\", 166, 0, 192);\n public static final IntValue CONTENT_BOOK_BOOKMARK_OFFSET_Y = intValue(\"content.bookBookmarkOffsetY\", 145, 0, 192);\n public static final BooleanValue CONTENT_SIGN_ENABLED = bool(\"content.signEnabled\", false);\n public static final EnumValue CONTENT_SIGN_CONTEXT_MODE =\n enumValue(\"content.signContextMode\", SignContextMode.AUTO, SignContextMode.class);\n public static final IntValue CONTENT_SIGN_RADIUS = intValue(\"content.signRadius\", 3, 1, 32);\n public static final BooleanValue CONTENT_ADVANCEMENT_ENABLED = bool(\"content.advancementEnabled\", false);\n public static final BooleanValue CONTENT_ENTITY_NAME_ENABLED = bool(\"content.entityNameEnabled\", false);\n public static final IntValue CONTENT_ENTITY_NAME_RADIUS = intValue(\"content.entityNameRadius\", 16, 1, 64);\n public static final BooleanValue CONTENT_TEXT_DISPLAY_ENABLED = bool(\"content.textDisplayEnabled\", false);\n public static final IntValue CONTENT_TEXT_DISPLAY_RADIUS = intValue(\"content.textDisplayRadius\", 16, 1, 64);\n public static final BooleanValue HUD_SCOREBOARD_ENABLED = bool(\"hud.scoreboardEnabled\", false);\n public static final BooleanValue HUD_BOSSBAR_ENABLED = bool(\"hud.bossbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_ENABLED = bool(\"hud.titleEnabled\", false);\n public static final BooleanValue HUD_ACTIONBAR_ENABLED = bool(\"hud.actionbarEnabled\", false);\n public static final BooleanValue HUD_TITLE_CONTEXT_ENABLED = bool(\"hud.titleContextEnabled\", false);\n public static final BooleanValue HUD_HISTORY_CHAT_ENABLED = bool(\"hud.historyChatEnabled\", false);\n public static final IntValue HUD_CAPTION_BATCH_INTERVAL_MS = intValue(\"hud.captionBatchIntervalMs\", 1500, 500, 10000);\n public static final IntValue HUD_CAPTION_COLLECT_WINDOW_MS = intValue(\"hud.captionCollectWindowMs\", 4500, 500, 30000);\n public static final ConfigValue TRANSLATION_STYLE_PROMPT = stringValue(\"hud.stylePrompt\", \"\");\n @Deprecated\n public static final ConfigValue HUD_STYLE_PROMPT = TRANSLATION_STYLE_PROMPT;\n public static final IntValue TERM_AUTO_DETECT_COUNT = intValue(\"terms.autoDetectCount\", 3, 1, 100);\n public static final BooleanValue TERM_AUTO_DETECT_ENABLED = bool(\"terms.autoDetectEnabled\", true);\n public static final BooleanValue CACHE_ENABLED = bool(\"cache.enabled\", true);\n public static final BooleanValue CACHE_SERVER_SHARE_ENABLED = bool(\"cache.serverShareEnabled\", false);\n\n public static final BooleanValue OCR_ENABLED = bool(\"ocr.enabled\", false);\n public static final BooleanValue OCR_USE_TRANSLATION_MODEL = bool(\"ocr.useTranslationModel\", true);\n public static final EnumValue OCR_API_FORMAT =\n enumValue(\"ocr.apiFormat\", ApiFormat.OPENAI_RESPONSES, ApiFormat.class);\n public static final ConfigValue OCR_API_URL =\n stringValue(\"ocr.apiUrl\", \"https://api.openai.com/v1/responses\");\n public static final ConfigValue OCR_API_KEY = stringValue(\"ocr.apiKey\", \"\");\n public static final ConfigValue OCR_MODEL =\n stringValue(\"ocr.model\", ApiFormat.OPENAI_RESPONSES.getDefaultModel());\n public static final IntValue OCR_REGION_WIDTH = intValue(\"ocr.regionWidth\", 420, 80, 1600);\n public static final IntValue OCR_REGION_HEIGHT = intValue(\"ocr.regionHeight\", 180, 40, 900);\n public static final IntValue OCR_REGION_X = intValue(\"ocr.regionX\", -1, -1, 16000);\n public static final IntValue OCR_REGION_Y = intValue(\"ocr.regionY\", -1, -1, 9000);\n public static final BooleanValue OCR_HISTORY_ENABLED = bool(\"ocr.historyEnabled\", false);\n\n public static final BooleanValue HOLD_ORIGINAL_ENABLED = bool(\"holdOriginal.enabled\", false);\n public static final IntValue HOLD_ORIGINAL_KEY_CHAT = intValue(\"holdOriginal.key.chat\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_ITEM = intValue(\"holdOriginal.key.tooltipItem\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TOOLTIP_HOVER = intValue(\"holdOriginal.key.tooltipHover\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOOK = intValue(\"holdOriginal.key.book\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SIGN = intValue(\"holdOriginal.key.sign\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ADVANCEMENT = intValue(\"holdOriginal.key.advancement\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ENTITY_NAME = intValue(\"holdOriginal.key.entityName\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TEXT_DISPLAY = intValue(\"holdOriginal.key.textDisplay\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_SCOREBOARD = intValue(\"holdOriginal.key.scoreboard\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_BOSSBAR = intValue(\"holdOriginal.key.bossbar\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_TITLE = intValue(\"holdOriginal.key.title\", -1, -1, 2000);\n public static final IntValue HOLD_ORIGINAL_KEY_ACTIONBAR = intValue(\"holdOriginal.key.actionbar\", -1, -1, 2000);\n\n public static IntValue getHoldOriginalKey(HoldOriginalFeature feature) {\n return switch (feature) {\n case CHAT -> HOLD_ORIGINAL_KEY_CHAT;\n case TOOLTIP_ITEM -> HOLD_ORIGINAL_KEY_TOOLTIP_ITEM;\n case TOOLTIP_HOVER -> HOLD_ORIGINAL_KEY_TOOLTIP_HOVER;\n case BOOK -> HOLD_ORIGINAL_KEY_BOOK;\n case SIGN -> HOLD_ORIGINAL_KEY_SIGN;\n case ADVANCEMENT -> HOLD_ORIGINAL_KEY_ADVANCEMENT;\n case ENTITY_NAME -> HOLD_ORIGINAL_KEY_ENTITY_NAME;\n case TEXT_DISPLAY -> HOLD_ORIGINAL_KEY_TEXT_DISPLAY;\n case SCOREBOARD -> HOLD_ORIGINAL_KEY_SCOREBOARD;\n case BOSSBAR -> HOLD_ORIGINAL_KEY_BOSSBAR;\n case TITLE -> HOLD_ORIGINAL_KEY_TITLE;\n case ACTIONBAR -> HOLD_ORIGINAL_KEY_ACTIONBAR;\n };\n }\n\n public static String normalizeDeepSeekModelId(String modelId) {\n if (modelId == null || modelId.isBlank()) {\n return DeepSeekModelPreset.V4_FLASH.getModelId();\n }\n\n String normalized = modelId.trim();\n String compact = normalized.toLowerCase().replace(\"_\", \"-\").replace(\" \", \"\");\n return switch (compact) {\n case \"deepseekv4flash\", \"deepseek-v4flash\", \"deepseek-v4-flsh\", \"deepseekv4flsh\", \"v4flash\", \"flash\" ->\n DeepSeekModelPreset.V4_FLASH.getModelId();\n case \"deepseekv4pro\", \"deepseek-v4pro\", \"v4pro\", \"pro\", \"reasoner\", \"deepseekreasoner\" ->\n DeepSeekModelPreset.V4_PRO.getModelId();\n case \"chat\", \"deepseekchat\" -> DeepSeekModelPreset.V4_FLASH.getModelId();\n default -> DeepSeekModelPreset.V4_FLASH.getModelId();\n };\n }\n\n public static String normalizeModelId(String modelId) {\n ApiFormat format = API_FORMAT.get();\n if (format == ApiFormat.DEEPSEEK_CHAT) {\n return normalizeDeepSeekModelId(modelId);\n }\n String normalized = modelId == null ? \"\" : modelId.trim();\n return normalized.isBlank() ? format.getDefaultModel() : normalized;\n }\n\n public static String normalizeApiUrl(String apiUrl) {\n if (apiUrl == null) {\n return DEFAULT_API_URL;\n }\n String normalized = apiUrl.trim();\n if (normalized.startsWith(\"\\\"\") && normalized.endsWith(\"\\\"\") && normalized.length() >= 2) {\n normalized = normalized.substring(1, normalized.length() - 1).trim();\n }\n while (normalized.endsWith(\"/\") && !normalized.endsWith(\"://\")) {\n normalized = normalized.substring(0, normalized.length() - 1);\n }\n return normalized.isBlank() ? DEFAULT_API_URL : normalized;\n }\n\n public static String validateApiUrl(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return \"API URL is not configured\";\n }\n try {\n URI uri = URI.create(apiUrl.trim());\n String scheme = uri.getScheme();\n String host = uri.getHost();\n if (scheme == null || (!scheme.equalsIgnoreCase(\"https\") && !scheme.equalsIgnoreCase(\"http\"))) {\n return \"API URL must start with http:// or https://\";\n }\n if (host == null || host.isBlank()) {\n return \"API URL does not contain a valid host\";\n }\n String normalizedHost = host.toLowerCase(Locale.ROOT);\n if (isExampleHost(normalizedHost)) {\n return \"API URL is an example placeholder; enter the real provider address\";\n }\n return \"\";\n } catch (IllegalArgumentException e) {\n return \"API URL is invalid\";\n }\n }\n\n private static boolean isExampleHost(String host) {\n return host.equals(\"example.com\") || host.endsWith(\".example.com\")\n || host.equals(\"example.net\") || host.endsWith(\".example.net\")\n || host.equals(\"example.org\") || host.endsWith(\".example.org\")\n || host.equals(\"example.test\") || host.endsWith(\".example.test\")\n || host.equals(\"example.invalid\") || host.endsWith(\".example.invalid\");\n }\n\n public static synchronized void init(Path configDir) {\n configFile = configDir.resolve(\"simple_translate-client.json\");\n load();\n }\n\n public static synchronized void load() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n if (!Files.exists(configFile)) {\n save();\n return;\n }\n String raw = Files.readString(configFile);\n if (raw == null || raw.isBlank()) {\n save();\n return;\n }\n JsonObject root = JsonParser.parseString(raw).getAsJsonObject();\n migrateLegacyKeys(root);\n loadedRoot = root.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n entry.getValue().read(root.get(entry.getKey()));\n normalizeLoadedValue(entry.getValue());\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to load config\", e);\n }\n }\n\n public static synchronized void save() {\n if (configFile == null) {\n return;\n }\n try {\n Files.createDirectories(configFile.getParent());\n JsonObject root = loadedRoot == null ? new JsonObject() : loadedRoot.deepCopy();\n for (Map.Entry> entry : VALUES.entrySet()) {\n root.add(entry.getKey(), entry.getValue().write());\n }\n loadedRoot = root.deepCopy();\n Files.writeString(configFile, GSON.toJson(root));\n } catch (IOException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to save config\", e);\n }\n }\n\n private static void migrateLegacyKeys(JsonObject root) {\n copyLegacyKey(root, \"content.bookButtonOffsetX\", CONTENT_BOOK_BOOKMARK_OFFSET_X.getKey());\n copyLegacyKey(root, \"content.bookButtonOffsetY\", CONTENT_BOOK_BOOKMARK_OFFSET_Y.getKey());\n removeObsoleteHudHistoryWindowKeys(root);\n migrateLegacyLanguageDefaults(root);\n }\n\n private static void removeObsoleteHudHistoryWindowKeys(JsonObject root) {\n if (root == null) {\n return;\n }\n root.remove(\"hud.history\" + \"OverlayEnabled\");\n root.remove(\"hud.history\" + \"Anchor\");\n root.remove(\"hud.history\" + \"OffsetX\");\n root.remove(\"hud.history\" + \"OffsetY\");\n root.remove(\"hud.history\" + \"OverlayWidth\");\n root.remove(\"hud.history\" + \"OverlayHeight\");\n root.remove(\"hud.history\" + \"ShowOriginal\");\n }\n\n private static void migrateLegacyLanguageDefaults(JsonObject root) {\n if (root.has(SOURCE_LANGUAGE.getKey())\n && root.get(SOURCE_LANGUAGE.getKey()).isJsonPrimitive()\n && \"en\".equalsIgnoreCase(root.get(SOURCE_LANGUAGE.getKey()).getAsString())) {\n root.add(SOURCE_LANGUAGE.getKey(), new JsonPrimitive(\"auto\"));\n }\n if (root.has(TARGET_LANGUAGE.getKey())\n && root.get(TARGET_LANGUAGE.getKey()).isJsonPrimitive()\n && \"zh\".equalsIgnoreCase(root.get(TARGET_LANGUAGE.getKey()).getAsString())) {\n root.add(TARGET_LANGUAGE.getKey(), new JsonPrimitive(\"zh_cn\"));\n }\n }\n\n private static void copyLegacyKey(JsonObject root, String oldKey, String newKey) {\n if (root.has(newKey) || !root.has(oldKey)) {\n return;\n }\n root.add(newKey, root.get(oldKey).deepCopy());\n }\n\n private static void normalizeLoadedValue(ConfigValue value) {\n value.set(value.get());\n }\n\n private static ConfigValue stringValue(String key, String defaultValue) {\n return register(new ConfigValue<>(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsString() : fallback,\n value -> value == null ? JsonNull.INSTANCE : new JsonPrimitive(value)));\n }\n\n private static BooleanValue bool(String key, boolean defaultValue) {\n return register(new BooleanValue(key, defaultValue));\n }\n\n private static IntValue intValue(String key, int defaultValue, int min, int max) {\n return register(new IntValue(key, defaultValue, min, max));\n }\n\n private static > EnumValue enumValue(String key, E defaultValue, Class enumClass) {\n return register(new EnumValue<>(key, defaultValue, enumClass));\n }\n\n private static > T register(T value) {\n VALUES.put(value.getKey(), value);\n return value;\n }\n\n public static class ConfigValue {\n private final String key;\n private final T defaultValue;\n private final BiFunction parser;\n private final Function serializer;\n private T value;\n\n public ConfigValue(String key, T defaultValue, BiFunction parser, Function serializer) {\n this.key = key;\n this.defaultValue = defaultValue;\n this.value = defaultValue;\n this.parser = parser;\n this.serializer = serializer;\n }\n\n public T get() {\n return value;\n }\n\n public String getKey() {\n return key;\n }\n\n public void set(T value) {\n this.value = value == null ? defaultValue : value;\n }\n\n private void read(JsonElement element) {\n try {\n this.value = parser.apply(element, defaultValue);\n if (this.value == null) {\n this.value = defaultValue;\n }\n } catch (Exception ignored) {\n this.value = defaultValue;\n }\n }\n\n private JsonElement write() {\n try {\n return serializer.apply(value);\n } catch (Exception ignored) {\n return JsonNull.INSTANCE;\n }\n }\n }\n\n public static class BooleanValue extends ConfigValue {\n public BooleanValue(String key, boolean defaultValue) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsBoolean() : fallback,\n JsonPrimitive::new);\n }\n }\n\n public static class IntValue extends ConfigValue {\n private final int min;\n private final int max;\n\n public IntValue(String key, int defaultValue, int min, int max) {\n super(\n key,\n defaultValue,\n (element, fallback) -> element != null && element.isJsonPrimitive() ? element.getAsInt() : fallback,\n JsonPrimitive::new);\n this.min = min;\n this.max = max;\n set(defaultValue);\n }\n\n @Override\n public void set(Integer value) {\n int raw = value == null ? get() : value;\n int clamped = Math.max(min, Math.min(max, raw));\n super.set(clamped);\n }\n }\n\n public static class EnumValue> extends ConfigValue {\n public EnumValue(String key, E defaultValue, Class enumClass) {\n super(\n key,\n defaultValue,\n (element, fallback) -> {\n if (element == null || !element.isJsonPrimitive()) {\n return fallback;\n }\n String raw = element.getAsString();\n try {\n return Enum.valueOf(enumClass, raw);\n } catch (Exception ignored) {\n return fallback;\n }\n },\n value -> new JsonPrimitive(value.name()));\n }\n }\n\n public enum TranslationMode {\n AUTO,\n BUTTON\n }\n\n public enum SignContextMode {\n AUTO,\n MANUAL\n }\n\n public enum ApiFormat {\n DEEPSEEK_CHAT(\"DeepSeek Chat\", \"deepseek-v4-flash\"),\n OPENAI_CHAT_COMPAT(\"OpenAI Chat Compatible\", \"gpt-4o-mini\"),\n OPENAI_RESPONSES(\"OpenAI Responses\", \"gpt-4.1-mini\"),\n ANTHROPIC_MESSAGES(\"Anthropic Messages\", \"claude-3-5-haiku-latest\"),\n GEMINI_GENERATE_CONTENT(\"Gemini generateContent\", \"gemini-1.5-flash\");\n\n private final String displayName;\n private final String defaultModel;\n\n ApiFormat(String displayName, String defaultModel) {\n this.displayName = displayName;\n this.defaultModel = defaultModel;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getDefaultModel() {\n return defaultModel;\n }\n }\n\n public enum DeepSeekModelPreset {\n V4_FLASH(\"DeepSeek V4 Flash\", \"deepseek-v4-flash\"),\n V4_PRO(\"DeepSeek V4 Pro\", \"deepseek-v4-pro\");\n\n private final String displayName;\n private final String modelId;\n\n DeepSeekModelPreset(String displayName, String modelId) {\n this.displayName = displayName;\n this.modelId = modelId;\n }\n\n public String getDisplayName() {\n return displayName;\n }\n\n public String getModelId() {\n return modelId;\n }\n\n public static DeepSeekModelPreset fromModelId(String modelId) {\n String normalized = normalizeDeepSeekModelId(modelId);\n for (DeepSeekModelPreset preset : values()) {\n if (!preset.modelId.isEmpty() && preset.modelId.equalsIgnoreCase(normalized)) {\n return preset;\n }\n }\n return V4_FLASH;\n }\n }\n\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.19.4/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745706.927204, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755575.802142, "n": 61591, "h": "6e9645a768c95133", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n private volatile ApiRequestProfile lastSuccessfulProfile;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = ModConfig.SOURCE_LANGUAGE.get();\n String targetLanguage = ModConfig.TARGET_LANGUAGE.get();\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface());\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n /** Output budget follows the Component JSON array, not the optional context preface. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int contextEnd = body.indexOf(\"[/CONTEXT]\");\n if (contextEnd >= 0) {\n body = body.substring(contextEnd + \"[/CONTEXT]\".length()).trim();\n }\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n SimpleTranslateMod.getLogger().debug(\n \"Translation API returned no assistant text; skipping identical automatic retry\");\n return null;\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n if (chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n && \"response.output_text.delta\".equals(chunk.get(\"type\").getAsString())\n && chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n lastSuccessfulProfile = profile;\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase();\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.6/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785745706.9973834, "n": 62885, "h": "36230f918e150b29", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = effectiveSourceLanguage(request);\n String targetLanguage = effectiveTargetLanguage(request);\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface(), request.promptContext());\n int exactTopLevelCount = countJsonArrayElements(source);\n if (exactTopLevelCount >= 0) {\n systemPrompt += \"\\nTHIS REQUEST CONTAINS EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS. RETURN EXACTLY \" + exactTopLevelCount\n + \" TOP-LEVEL ARRAY ELEMENTS IN THE SAME ORDER.\";\n }\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n private static String effectiveSourceLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.sourceLanguage();\n return value == null || value.isBlank() ? ModConfig.SOURCE_LANGUAGE.get() : value;\n }\n\n private static String effectiveTargetLanguage(TranslationRequest request) {\n String value = request == null ? \"\" : request.targetLanguage();\n return value == null || value.isBlank() ? ModConfig.TARGET_LANGUAGE.get() : value;\n }\n\n /** Output budget follows only the top-level user array; context is carried in the system prompt. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"API response did not contain assistant text output\");\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n if (\"API response did not contain assistant text output\".equals(invalid.getMessage())) {\n // A successful HTTP response with no assistant payload is a\n // transient provider completion failure, not a valid empty\n // translation. Let the bounded request queue retry it instead\n // of freezing every tooltip/HUD cache key on the first blank.\n throw new TranslationRequestQueue.RetryableTranslationException(\n invalid.getMessage(), invalid);\n }\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n // Only plain text deltas are translation output; reasoning or\n // refusal deltas and other typed events must never be appended.\n String type = chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n ? chunk.get(\"type\").getAsString() : \"\";\n if (type.isBlank() || \"response.output_text.delta\".equals(type)) {\n result.append(chunk.get(\"delta\").getAsString());\n }\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase(java.util.Locale.ROOT);\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT, LOCAL_OLLAMA -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n if (apiKey != null && !apiKey.isEmpty()) {\n return true;\n }\n return ModConfig.API_FORMAT.get() == ModConfig.ApiFormat.LOCAL_OLLAMA;\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}, "2b53e35e17f09f454f1cf4561fa7e09b1f70f18f": {"t": 1785755576.2744577, "n": 61591, "h": "6e9645a768c95133", "c": "package com.yourname.simpletranslate.transport;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.api.TokenUsage;\nimport com.yourname.simpletranslate.api.TranslationDiagnostics;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.api.TranslationService;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.core.TranslationCacheKeys;\n\nimport java.net.URI;\nimport java.net.URLEncoder;\nimport java.io.BufferedReader;\nimport java.io.IOException;\nimport java.io.InputStream;\nimport java.io.InputStreamReader;\nimport java.net.http.HttpClient;\nimport java.net.http.HttpRequest;\nimport java.net.http.HttpResponse;\nimport java.nio.charset.StandardCharsets;\nimport java.time.Duration;\nimport java.util.ArrayList;\nimport java.util.LinkedHashMap;\nimport java.util.LinkedHashSet;\nimport java.util.List;\nimport java.util.Map;\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\nimport java.util.concurrent.CompletionException;\nimport java.util.concurrent.ConcurrentHashMap;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.Executors;\nimport java.util.concurrent.Future;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.TimeoutException;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * DeepSeek API translation service implementation.\n */\npublic class DeepSeekTranslationService implements TranslationService {\n\n private static final String SERVICE_NAME = \"DeepSeek\";\n /** Parallelism for language-detection probes only; global translation concurrency is in {@link TranslationRequestQueue}. */\n private static final int MAX_CONCURRENT_REQUESTS = 2;\n private static final int MAX_REQUEST_ATTEMPTS = 3;\n private static final int MIN_TRANSLATION_OUTPUT_TOKENS = 128;\n private static final int MAX_TRANSLATION_OUTPUT_TOKENS = 16384;\n private static final long WARN_SLOW_FIRST_CHUNK_MS = 8000L;\n private static final long WARN_SLOW_TOTAL_MS = 30000L;\n private static final long MAX_STREAM_TOTAL_MS = 45000L;\n private static final int MODEL_ACCESS_PROBE_MAX_TOKENS = 256;\n private static final Pattern HTML_TITLE_PATTERN = Pattern.compile(\"(?is)]*>(.*?)\");\n private static final Set REPORTED_INVALID_API_URLS = ConcurrentHashMap.newKeySet();\n private static final ExecutorService DETECTION_EXECUTOR = Executors.newFixedThreadPool(MAX_CONCURRENT_REQUESTS, runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-DeepSeekProbe\");\n thread.setDaemon(true);\n return thread;\n });\n private static final ExecutorService STREAM_READER_EXECUTOR = Executors.newCachedThreadPool(runnable -> {\n Thread thread = new Thread(runnable, \"SimpleTranslate-StreamReader\");\n thread.setDaemon(true);\n return thread;\n });\n private final HttpClient httpClient;\n private final Gson gson;\n private volatile ApiRequestProfile lastSuccessfulProfile;\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n public DeepSeekTranslationService() {\n this.httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n this.gson = new Gson();\n }\n\n public void shutdown() {\n DETECTION_EXECUTOR.shutdownNow();\n STREAM_READER_EXECUTOR.shutdownNow();\n }\n\n @Override\n public CompletableFuture translate(TranslationRequest request) {\n if (request == null || request.lines().isEmpty()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"empty-request\"));\n }\n if (!isReady()) {\n return CompletableFuture.completedFuture(new TranslationResult.Failed(\"api-key-not-configured\"));\n }\n String sourceLanguage = ModConfig.SOURCE_LANGUAGE.get();\n String targetLanguage = ModConfig.TARGET_LANGUAGE.get();\n String source = String.join(\"\\n\", request.lines());\n if (!isJsonArrayPayload(source)) {\n return CompletableFuture.completedFuture(\n new TranslationResult.Failed(\"component-json-required\"));\n }\n String systemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n sourceLanguage, targetLanguage, request.terms(), request.surface());\n CompletableFuture future = sendRequest(systemPrompt, source,\n estimateDirectMaxTokens(source, request.maxTokenMultiplier()), request.surface());\n return future.handle((payload, error) -> {\n if (error != null) {\n String reason = error.getMessage();\n return new TranslationResult.Failed(\n reason == null || reason.isBlank() ? error.getClass().getSimpleName() : reason);\n }\n if (payload == null || payload.isBlank()) {\n return new TranslationResult.Failed(\"empty-response\");\n }\n return new TranslationResult.Success(payload);\n });\n }\n\n private static boolean isJsonArrayPayload(String source) {\n if (source == null) {\n return false;\n }\n String trimmed = source.trim();\n return trimmed.startsWith(\"[\") && trimmed.endsWith(\"]\");\n }\n\n /** Output budget follows the Component JSON array, not the optional context preface. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Multiplier is retained for callers that submit unusually large JSON arrays. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int contextEnd = body.indexOf(\"[/CONTEXT]\");\n if (contextEnd >= 0) {\n body = body.substring(contextEnd + \"[/CONTEXT]\".length()).trim();\n }\n int elementCount = countJsonArrayElements(body);\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int elementEstimate = elementCount * 192 + 384;\n int estimated = Math.max(charEstimate, elementEstimate);\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n estimated = MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n estimated = Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n int scale = Math.max(1, multiplier);\n return Math.min(estimated * scale, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n /** Counts top-level elements in a JSON array string (for token estimation). */\n private static int countJsonArrayElements(String json) {\n if (json == null || json.isBlank()) {\n return 0;\n }\n try {\n JsonElement root = JsonParser.parseString(json.trim());\n if (root.isJsonArray()) {\n return root.getAsJsonArray().size();\n }\n } catch (Exception ignored) {\n }\n return 0;\n }\n\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationDiagnostics.ApiDetection(false, \"\", \"\", \"\", 0, \"API key not configured\"));\n }\n\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n String systemPrompt = \"You are a connectivity probe for a Minecraft translation client. \"\n + \"Respond briefly and naturally.\";\n String userPrompt = \"Probe\";\n\n return CompletableFuture.supplyAsync(() -> probeApi(profiles, systemPrompt, userPrompt), DETECTION_EXECUTOR);\n }\n\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelDetection(\n false, ModConfig.normalizeApiUrl(apiUrl), 0, List.of(),\n \"Model list detection is not supported for \" + (apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat)));\n }\n\n public CompletableFuture verifyModelAccess(String apiKey, String apiUrl,\n String modelId, ModConfig.ApiFormat apiFormat) {\n String key = sanitizeApiKey(apiKey);\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.API_FORMAT.get() : apiFormat;\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(modelId)\n : (modelId == null || modelId.isBlank() ? format.getDefaultModel() : modelId.trim());\n if (key.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationDiagnostics.ModelAccess(\n false, model, 0, urlError));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String surface) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String apiUrl = ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n String urlError = ModConfig.validateApiUrl(ModConfig.DEEPSEEK_API_URL.get());\n if (!urlError.isBlank()) {\n if (REPORTED_INVALID_API_URLS.add(apiUrl + \"|\" + urlError)) {\n SimpleTranslateMod.getLogger().warn(\"Translation requests disabled for the current API URL: {}\", urlError);\n }\n return CompletableFuture.completedFuture(null);\n }\n\n SimpleTranslateMod.getLogger().debug(\"Sending translation request to {} with model {} format {}\", apiUrl, model, apiFormat);\n SimpleTranslateMod.getLogger().debug(\"Text to translate: {}\", userPrompt);\n List profiles = buildRequestProfiles(apiUrl, apiFormat, model);\n\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestAsync(profiles, apiKey, model, apiFormat.name(), surface,\n systemPrompt, userPrompt, maxTokens));\n }\n\n private CompletableFuture sendRequestAsync(List profiles, String apiKey, String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n return sendRequestAsync(orderedProfiles, 0, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n }\n\n private CompletableFuture sendRequestAsync(List profiles, int index,\n String apiKey, String model, String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (index >= profiles.size()) {\n return CompletableFuture.completedFuture(null);\n }\n ApiRequestProfile profile = profiles.get(index);\n return sendRequestWithProfileAsync(profile, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens)\n .thenCompose(translated -> {\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return CompletableFuture.completedFuture(translated);\n }\n return sendRequestAsync(profiles, index + 1, apiKey, model, apiFormat, surface,\n systemPrompt, userPrompt, maxTokens);\n });\n }\n\n private CompletableFuture sendRequestWithProfileAsync(ApiRequestProfile profile, String apiKey,\n String model,\n String apiFormat, String surface,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return CompletableFuture.completedFuture(null);\n }\n\n long startedAt = System.nanoTime();\n long startedAtMs = System.currentTimeMillis();\n HttpRequest request;\n try {\n request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n } catch (RuntimeException e) {\n return CompletableFuture.failedFuture(e);\n }\n\n return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())\n .thenApplyAsync(response -> handleTranslationResponse(response, startedAt, startedAtMs,\n apiFormat, model, surface), STREAM_READER_EXECUTOR)\n .handle((translated, error) -> handleTranslationFailure(translated, error));\n }\n\n private String sendRequestWithProfileForProbe(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n try {\n return sendRequestWithProfileAsync(profile, apiKey, model, null, null,\n systemPrompt, userPrompt, maxTokens).join();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new RuntimeException(cause);\n }\n }\n\n private String handleTranslationResponse(HttpResponse response, long startedAt, long startedAtMs,\n String apiFormat, String model, String surface) {\n try {\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n ParsedTranslationResponse parsed = parseResponse(response.body(), startedAt);\n String result = parsed.text();\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > WARN_SLOW_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"DeepSeek translation request completed slowly in {} ms\",\n elapsedMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"DeepSeek translation request completed in {} ms\",\n elapsedMs);\n }\n if (result != null && !result.isBlank()) {\n SimpleTranslateMod.getLogger().debug(\"Translation result: {}\", result);\n recordTokenUsage(apiFormat, model, startedAtMs, surface, parsed.usage());\n return result;\n }\n SimpleTranslateMod.getLogger().debug(\n \"Translation API returned no assistant text; skipping identical automatic retry\");\n return null;\n }\n\n String errorBody = readErrorBody(response.body());\n if (isRetryableStatus(response.statusCode())) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (errorBody == null || errorBody.isBlank() ? \"\" : \": \" + errorBody));\n }\n\n SimpleTranslateMod.getLogger().warn(\"DeepSeek API error: {} - {}\",\n response.statusCode(), errorBody);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n }\n }\n\n private String handleTranslationFailure(String translated, Throwable error) {\n if (error == null) {\n return translated;\n }\n Throwable cause = unwrapCompletion(error);\n if (cause instanceof TranslationRequestQueue.RetryableTranslationException retryable) {\n throw retryable;\n }\n if (cause instanceof IOException ioException) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n ioException.getMessage() == null ? \"I/O error\" : ioException.getMessage(), ioException);\n }\n if (cause instanceof InvalidApiResponseException invalid) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\",\n invalid.getMessage());\n return null;\n }\n if (cause instanceof java.util.concurrent.CancellationException canceled) {\n throw canceled;\n }\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\",\n cause.getMessage() == null ? cause.getClass().getSimpleName() : cause.getMessage(), cause);\n return null;\n }\n\n private static Throwable unwrapCompletion(Throwable error) {\n Throwable current = error;\n while ((current instanceof CompletionException || current instanceof java.util.concurrent.ExecutionException)\n && current.getCause() != null) {\n current = current.getCause();\n }\n return current;\n }\n\n private static String buildQueueKey(String apiUrl, String model, String apiFormat,\n String systemPrompt, String userPrompt, int maxTokens) {\n return \"deepseek:\" + TranslationCacheKeys.hashSource(\n (apiUrl == null ? \"\" : apiUrl) + \"\\u0001\"\n + (model == null ? \"\" : model) + \"\\u0001\"\n + (apiFormat == null ? \"\" : apiFormat) + \"\\u0001\"\n + maxTokens + \"\\u0001\"\n + (systemPrompt == null ? \"\" : systemPrompt) + \"\\u0001\"\n + (userPrompt == null ? \"\" : userPrompt));\n }\n\n private static TranslationRequestQueue.Priority priorityForSurface(String surface) {\n String value = surface == null ? \"\" : surface.toLowerCase();\n if (value.startsWith(\"sign.manual\")) {\n return TranslationRequestQueue.Priority.MANUAL_SIGN;\n }\n if (value.startsWith(\"tooltip.\") || value.startsWith(\"hover.\")) {\n return TranslationRequestQueue.Priority.INTERACTIVE;\n }\n if (value.startsWith(\"chat.\")) {\n return TranslationRequestQueue.Priority.CHAT;\n }\n if (value.startsWith(\"hud.title_group\") || value.startsWith(\"hud.title.\")\n || value.startsWith(\"hud.subtitle.\") || value.startsWith(\"title.\")) {\n return TranslationRequestQueue.Priority.TITLE_URGENT;\n }\n if (value.startsWith(\"hud.actionbar.\") || value.startsWith(\"actionbar.\")) {\n return TranslationRequestQueue.Priority.ACTIONBAR_URGENT;\n }\n if (value.startsWith(\"hud.\") || value.startsWith(\"title.\") || value.startsWith(\"actionbar.\")\n || value.startsWith(\"scoreboard\") || value.startsWith(\"bossbar.\")) {\n return TranslationRequestQueue.Priority.HUD;\n }\n if (value.startsWith(\"book\")) {\n return TranslationRequestQueue.Priority.BOOK;\n }\n if (value.contains(\".fcs.\")) {\n return TranslationRequestQueue.Priority.FRAGMENT;\n }\n if (value.startsWith(\"advancement.\")) {\n return TranslationRequestQueue.Priority.ADVANCEMENT_INTERACTIVE;\n }\n return TranslationRequestQueue.Priority.NORMAL;\n }\n\n private static long elapsedMillis(long startedAt) {\n return (System.nanoTime() - startedAt) / 1_000_000L;\n }\n\n private static boolean isRetryableStatus(int statusCode) {\n return statusCode == 429 || statusCode >= 500;\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.trim().startsWith(\"data:\")) {\n return parseStreamingResponse(responseBody).text();\n }\n return parsePlainResponse(responseBody).text();\n }\n\n private ParsedTranslationResponse parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n Future readerTask =\n STREAM_READER_EXECUTOR.submit(() -> parseResponseBlocking(responseBody, startedAt));\n try {\n long remainingMs = Math.max(1L, MAX_STREAM_TOTAL_MS - elapsedMillis(startedAt));\n return readerTask.get(remainingMs, TimeUnit.MILLISECONDS);\n } catch (TimeoutException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n } catch (InterruptedException e) {\n closeQuietly(responseBody);\n readerTask.cancel(true);\n Thread.currentThread().interrupt();\n throw new IOException(\"Translation stream interrupted\", e);\n } catch (java.util.concurrent.ExecutionException e) {\n Throwable cause = e.getCause();\n if (cause instanceof IOException ioException) {\n throw ioException;\n }\n if (cause instanceof RuntimeException runtimeException) {\n throw runtimeException;\n }\n throw new IOException(\"Translation stream failed\", cause);\n }\n }\n\n private ParsedTranslationResponse parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return ParsedTranslationResponse.empty();\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n boolean sawData = false;\n long firstChunkMs = -1L;\n try (BufferedReader reader = new BufferedReader(new InputStreamReader(responseBody, StandardCharsets.UTF_8))) {\n String line;\n while ((line = reader.readLine()) != null) {\n long elapsedMs = elapsedMillis(startedAt);\n if (elapsedMs > MAX_STREAM_TOTAL_MS) {\n SimpleTranslateMod.getLogger().warn(\"Translation stream exceeded {} ms; aborting this response\",\n MAX_STREAM_TOTAL_MS);\n return ParsedTranslationResponse.empty();\n }\n raw.append(line).append('\\n');\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n\n String json = trimmed.substring(5).trim();\n if (json.isEmpty()) {\n continue;\n }\n if (\"[DONE]\".equals(json)) {\n break;\n }\n\n sawData = true;\n if (firstChunkMs < 0L) {\n firstChunkMs = elapsedMillis(startedAt);\n if (firstChunkMs > WARN_SLOW_FIRST_CHUNK_MS) {\n SimpleTranslateMod.getLogger().warn(\"First translation chunk took {} ms\", firstChunkMs);\n } else {\n SimpleTranslateMod.getLogger().debug(\"First translation chunk arrived in {} ms\", firstChunkMs);\n }\n }\n\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n\n if (sawData) {\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n return parsePlainResponse(raw.toString());\n }\n\n private static void closeQuietly(InputStream stream) {\n if (stream == null) {\n return;\n }\n try {\n stream.close();\n } catch (IOException ignored) {\n // Best-effort cancellation path.\n }\n }\n\n private ParsedTranslationResponse parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\n TokenUsage[] usageHolder = new TokenUsage[1];\n try {\n for (String line : responseBody.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (!trimmed.startsWith(\"data:\")) {\n continue;\n }\n String json = trimmed.substring(5).trim();\n if (json.isEmpty() || \"[DONE]\".equals(json)) {\n continue;\n }\n appendStreamingChunk(result, json, usageHolder);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return ParsedTranslationResponse.empty();\n }\n return new ParsedTranslationResponse(result.toString(), usageHolder[0]);\n }\n\n private ParsedTranslationResponse parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return ParsedTranslationResponse.empty();\n }\n String trimmed = responseBody.trim();\n if (looksLikeHtml(trimmed)) {\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed));\n }\n try {\n JsonObject response = JsonParser.parseString(trimmed).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return new ParsedTranslationResponse(raw, extractUsageFromResponse(response));\n }\n throw new InvalidApiResponseException(\"API response did not contain assistant text output\");\n } catch (Exception e) {\n if (e instanceof InvalidApiResponseException invalid) {\n throw invalid;\n }\n throw new InvalidApiResponseException(invalidApiResponseMessage(trimmed), e);\n }\n }\n\n private String extractJsonObjectPayload(String text) {\n if (text == null) {\n return null;\n }\n String trimmed = text.trim();\n if (trimmed.startsWith(\"{\") && trimmed.endsWith(\"}\")) {\n return trimmed;\n }\n\n int start = trimmed.indexOf('{');\n if (start < 0) {\n return null;\n }\n\n boolean inString = false;\n boolean escaped = false;\n int depth = 0;\n for (int i = start; i < trimmed.length(); i++) {\n char c = trimmed.charAt(i);\n if (escaped) {\n escaped = false;\n continue;\n }\n if (c == '\\\\') {\n escaped = true;\n continue;\n }\n if (c == '\"') {\n inString = !inString;\n continue;\n }\n if (inString) {\n continue;\n }\n if (c == '{') {\n depth++;\n } else if (c == '}') {\n depth--;\n if (depth == 0) {\n return trimmed.substring(start, i + 1).trim();\n }\n }\n }\n return null;\n }\n\n private void appendStreamingChunk(StringBuilder result, String json, TokenUsage[] usageHolder) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\n TokenUsage usage = extractUsageFromResponse(chunk);\n if (usage != null) {\n usageHolder[0] = usage;\n }\n if (chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n if (chunk.has(\"type\") && !chunk.get(\"type\").isJsonNull()\n && \"response.output_text.delta\".equals(chunk.get(\"type\").getAsString())\n && chunk.has(\"delta\") && !chunk.get(\"delta\").isJsonNull()) {\n result.append(chunk.get(\"delta\").getAsString());\n return;\n }\n JsonArray choices = chunk.getAsJsonArray(\"choices\");\n if (choices == null || choices.isEmpty()) {\n return;\n }\n JsonObject delta = choices.get(0).getAsJsonObject().getAsJsonObject(\"delta\");\n if (delta != null && delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n result.append(delta.get(\"content\").getAsString());\n }\n }\n\n private void recordTokenUsage(String apiFormat, String model, long requestStartMs, String surface,\n TokenUsage usage) {\n if (!ModConfig.TOKEN_MONITOR_ENABLED.get() || usage == null\n || apiFormat == null || apiFormat.isBlank()) {\n return;\n }\n try {\n long elapsedMs = System.currentTimeMillis() - requestStartMs;\n int prompt = Math.max(0, usage.promptTokens());\n int completion = Math.max(0, usage.completionTokens());\n int total = usage.totalTokens() > 0 ? usage.totalTokens() : prompt + completion;\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return;\n }\n TokenUsageMonitor.record(new TokenUsage(\n apiFormat, model, prompt, completion, total,\n elapsedMs, System.currentTimeMillis(), surface));\n } catch (Exception ignored) {\n }\n }\n\n private TokenUsage extractUsageFromResponse(JsonObject response) {\n if (response == null) {\n return null;\n }\n if (response.has(\"usage\") && response.get(\"usage\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usage\"));\n if (usage != null) {\n return usage;\n }\n }\n if (response.has(\"usageMetadata\") && response.get(\"usageMetadata\").isJsonObject()) {\n TokenUsage usage = extractUsage(response.getAsJsonObject(\"usageMetadata\"));\n if (usage != null) {\n return usage;\n }\n }\n return extractUsage(response);\n }\n\n private TokenUsage extractUsage(JsonObject usage) {\n if (usage == null) {\n return null;\n }\n try {\n int prompt = firstInt(usage,\n \"prompt_tokens\", \"promptTokens\", \"input_tokens\", \"inputTokens\",\n \"promptTokenCount\", \"inputTokenCount\");\n int completion = firstInt(usage,\n \"completion_tokens\", \"completionTokens\", \"output_tokens\", \"outputTokens\",\n \"candidatesTokenCount\", \"completionTokenCount\", \"outputTokenCount\");\n int thoughts = firstInt(usage, \"thoughtsTokenCount\", \"reasoning_tokens\", \"reasoningTokens\");\n if (thoughts > 0) {\n completion += thoughts;\n }\n int total = firstInt(usage, \"total_tokens\", \"totalTokens\", \"totalTokenCount\");\n if (total <= 0) {\n total = prompt + completion;\n }\n if (prompt <= 0 && completion <= 0 && total <= 0) {\n return null;\n }\n return new TokenUsage(\"\", \"\", prompt, completion, total, 0, 0, \"\");\n } catch (Exception ignored) {\n return null;\n }\n }\n\n private static int firstInt(JsonObject object, String... names) {\n if (object == null || names == null) {\n return 0;\n }\n for (String name : names) {\n if (name == null || !object.has(name) || object.get(name).isJsonNull()) {\n continue;\n }\n try {\n return Math.max(0, object.get(name).getAsInt());\n } catch (Exception ignored) {\n }\n }\n return 0;\n }\n\n private TranslationDiagnostics.ApiDetection probeApi(List profiles, String systemPrompt,\n String userPrompt) {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n ModConfig.ApiFormat apiFormat = ModConfig.API_FORMAT.get();\n String model = apiFormat == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? ModConfig.normalizeDeepSeekModelId(ModConfig.DEEPSEEK_MODEL.get())\n : ModConfig.normalizeModelId(ModConfig.DEEPSEEK_MODEL.get());\n String lastMessage = apiFormat.getDisplayName() + \" endpoint did not respond successfully\";\n for (ApiRequestProfile profile : profiles) {\n try {\n String result = sendRequestWithProfileForProbe(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationDiagnostics.ApiDetection(true, profile.kind().name(),\n profile.kind() == EndpointKind.GEMINI_GENERATE_CONTENT ? \"API_KEY\" : \"BEARER\",\n profile.endpointUrl(), 200, \"API responded successfully\");\n }\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n lastMessage = e.getMessage() == null ? \"Retryable API detection failure\" : e.getMessage();\n }\n }\n return new TranslationDiagnostics.ApiDetection(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationDiagnostics.ModelAccess probeModelAccess(String apiUrl, String apiKey, String modelId,\n ModConfig.ApiFormat apiFormat) {\n List profiles = buildRequestProfiles(apiUrl, apiFormat, modelId);\n String lastMessage = \"No compatible \" + apiFormat.getDisplayName() + \" endpoint responded\";\n int lastStatus = 0;\n String probeSystemPrompt = JsonPassthroughPrompts.buildSystemPrompt(\n \"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(), \"manager.model_probe\");\n String probeUserPrompt = \"[{\\\"text\\\":\\\"Hello\\\"}]\";\n\n for (ApiRequestProfile profile : profiles) {\n try {\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(profile.endpointUrl()))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(\n buildRequestBody(profile, modelId, probeSystemPrompt, probeUserPrompt,\n MODEL_ACCESS_PROBE_MAX_TOKENS, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.sendAsync(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream()).join();\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n String translated = parseResponse(body);\n if (isValidModelAccessProbeResponse(translated)) {\n lastSuccessfulProfile = profile;\n return new TranslationDiagnostics.ModelAccess(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = \"Model responded but did not follow the translation protocol\";\n continue;\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : compactErrorBody(body);\n } catch (InvalidApiResponseException e) {\n lastMessage = e.getMessage() == null ? \"Invalid API response\" : e.getMessage();\n } catch (CompletionException e) {\n Throwable cause = unwrapCompletion(e);\n lastMessage = cause.getMessage() == null ? \"Model verification failed\" : cause.getMessage();\n } catch (RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationDiagnostics.ModelAccess(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n try {\n String trimmed = value.trim();\n if (trimmed.startsWith(\"```\")) {\n int firstNewline = trimmed.indexOf('\\n');\n int lastFence = trimmed.lastIndexOf(\"```\");\n if (firstNewline >= 0 && lastFence > firstNewline) {\n trimmed = trimmed.substring(firstNewline + 1, lastFence).trim();\n }\n }\n return JsonParser.parseString(trimmed).isJsonArray()\n && JsonParser.parseString(trimmed).getAsJsonArray().size() == 1;\n } catch (Exception ignored) {\n return false;\n }\n }\n\n private static boolean looksLikeHtml(String value) {\n if (value == null) {\n return false;\n }\n String trimmed = value.stripLeading().toLowerCase();\n return trimmed.startsWith(\"]+>\", \" \")\n .replace('\\n', ' ')\n .replace('\\r', ' ')\n .replaceAll(\"\\\\s+\", \" \")\n .trim();\n int limit = Math.max(16, maxLength);\n return compact.length() > limit ? compact.substring(0, limit - 3) + \"...\" : compact;\n }\n\n private static final class InvalidApiResponseException extends RuntimeException {\n private InvalidApiResponseException(String message) {\n super(message);\n }\n\n private InvalidApiResponseException(String message, Throwable cause) {\n super(message, cause);\n }\n }\n\n private List buildRequestProfiles(String apiUrl, ModConfig.ApiFormat apiFormat, String model) {\n ModConfig.ApiFormat format = apiFormat == null ? ModConfig.ApiFormat.DEEPSEEK_CHAT : apiFormat;\n String base = ModConfig.normalizeApiUrl(apiUrl);\n return switch (format) {\n case DEEPSEEK_CHAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS, true));\n case OPENAI_CHAT_COMPAT -> List.of(new ApiRequestProfile(base, resolveChatEndpoint(base),\n EndpointKind.CHAT_COMPLETIONS,\n isOfficialDeepSeekEndpoint(base) || isDeepSeekReasoningModel(model)));\n case OPENAI_RESPONSES -> List.of(new ApiRequestProfile(base, resolveResponsesEndpoint(base),\n EndpointKind.RESPONSES, false));\n case ANTHROPIC_MESSAGES -> List.of(new ApiRequestProfile(base, resolveAnthropicMessagesEndpoint(base),\n EndpointKind.ANTHROPIC_MESSAGES, false));\n case GEMINI_GENERATE_CONTENT -> List.of(new ApiRequestProfile(base,\n resolveGeminiGenerateContentEndpoint(base, model),\n EndpointKind.GEMINI_GENERATE_CONTENT, false));\n };\n }\n\n private static boolean isOfficialDeepSeekEndpoint(String apiUrl) {\n if (apiUrl == null || apiUrl.isBlank()) {\n return false;\n }\n try {\n String host = URI.create(apiUrl).getHost();\n if (host == null) {\n return false;\n }\n String normalized = host.toLowerCase(java.util.Locale.ROOT);\n return normalized.equals(\"deepseek.com\") || normalized.endsWith(\".deepseek.com\");\n } catch (RuntimeException ignored) {\n return false;\n }\n }\n\n private static boolean isDeepSeekReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"deepseek-\")\n && (normalized.contains(\"reasoner\")\n || normalized.contains(\"r1\")\n || normalized.contains(\"v4\"));\n }\n\n private static boolean isOpenAiReasoningModel(String model) {\n if (model == null || model.isBlank()) {\n return false;\n }\n String normalized = model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"o1\")\n || normalized.startsWith(\"o3\")\n || normalized.startsWith(\"o4\")\n || normalized.startsWith(\"gpt-5\");\n }\n\n private static String openAiReasoningEffort(String model, boolean enabled) {\n if (enabled) {\n return \"medium\";\n }\n String normalized = model == null ? \"\" : model.toLowerCase(java.util.Locale.ROOT);\n return normalized.startsWith(\"gpt-5\") ? \"minimal\" : \"low\";\n }\n\n private String resolveChatEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.isBlank()) {\n return \"\";\n }\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\")) {\n return normalized + \"/chat/completions\";\n }\n if (normalized.endsWith(\"/v1/\")) {\n return normalized + \"chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private String resolveResponsesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/responses\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/responses\";\n }\n return normalized + \"/responses\";\n }\n\n private String resolveAnthropicMessagesEndpoint(String apiUrl) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\"/messages\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/messages\";\n }\n return normalized + \"/v1/messages\";\n }\n\n private String resolveGeminiGenerateContentEndpoint(String apiUrl, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n if (normalized.contains(\":generateContent\")) {\n return normalized;\n }\n String encodedModel = URLEncoder.encode(model == null || model.isBlank()\n ? ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT.getDefaultModel() : model, StandardCharsets.UTF_8);\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1beta\")) {\n return normalized + \"/models/\" + encodedModel + \":generateContent\";\n }\n if (normalized.endsWith(\"/v1/\") || normalized.endsWith(\"/v1beta/\")) {\n return normalized + \"models/\" + encodedModel + \":generateContent\";\n }\n return normalized + \"/v1beta/models/\" + encodedModel + \":generateContent\";\n }\n\n private HttpRequest buildRequest(ApiRequestProfile profile, String apiKey, String model, String systemPrompt,\n String userPrompt, int maxTokens) {\n JsonObject requestBody = buildRequestBody(profile, model, systemPrompt, userPrompt, maxTokens, true);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile, apiKey)))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(requestBody)))\n .timeout(Duration.ofSeconds(60));\n\n applyAuthHeader(builder, profile, apiKey);\n return builder.build();\n }\n\n private JsonObject buildRequestBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n return switch (profile.kind()) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(profile, model, systemPrompt, userPrompt, maxTokens, streaming);\n case RESPONSES -> buildResponsesBody(model, systemPrompt, userPrompt, maxTokens, streaming);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, systemPrompt, userPrompt, maxTokens);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(model, systemPrompt, userPrompt, maxTokens);\n };\n }\n\n private JsonObject buildChatCompletionsBody(ApiRequestProfile profile, String model, String systemPrompt,\n String userPrompt, int maxTokens, boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n boolean openAiReasoning = !profile.supportsThinking() && isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 4096);\n } else if (openAiReasoning) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n\n if (!thinkingEnabled && !openAiReasoning) {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\n } else if (openAiReasoning) {\n requestBody.addProperty(\"reasoning_effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n }\n\n JsonArray messages = new JsonArray();\n\n JsonObject systemMessage = new JsonObject();\n systemMessage.addProperty(\"role\", \"system\");\n systemMessage.addProperty(\"content\", systemPrompt);\n messages.add(systemMessage);\n\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n\n requestBody.add(\"messages\", messages);\n if (streaming) {\n JsonObject streamOptions = new JsonObject();\n streamOptions.addProperty(\"include_usage\", true);\n requestBody.add(\"stream_options\", streamOptions);\n }\n return requestBody;\n }\n\n private JsonObject buildResponsesBody(String model, String systemPrompt, String userPrompt, int maxTokens,\n boolean streaming) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n requestBody.addProperty(\"stream\", streaming);\n boolean reasoningModel = isOpenAiReasoningModel(model);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (reasoningModel) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject reasoning = new JsonObject();\n reasoning.addProperty(\"effort\",\n openAiReasoningEffort(model, ModConfig.DEEPSEEK_THINKING_ENABLED.get()));\n requestBody.add(\"reasoning\", reasoning);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_output_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"instructions\", systemPrompt);\n\n JsonArray input = new JsonArray();\n JsonObject message = new JsonObject();\n message.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"type\", \"input_text\");\n part.addProperty(\"text\", userPrompt);\n content.add(part);\n message.add(\"content\", content);\n input.add(message);\n requestBody.add(\"input\", input);\n return requestBody;\n }\n\n private JsonObject buildAnthropicMessagesBody(String model, String systemPrompt, String userPrompt, int maxTokens) {\n JsonObject requestBody = new JsonObject();\n requestBody.addProperty(\"model\", model);\n boolean thinkingEnabled = ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (thinkingEnabled) {\n outputTokens = Math.max(outputTokens, 2048);\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", \"enabled\");\n thinking.addProperty(\"budget_tokens\", 1024);\n requestBody.add(\"thinking\", thinking);\n } else {\n requestBody.addProperty(\"temperature\", 0.0);\n }\n requestBody.addProperty(\"max_tokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n requestBody.addProperty(\"system\", systemPrompt);\n\n JsonArray messages = new JsonArray();\n JsonObject userMessage = new JsonObject();\n userMessage.addProperty(\"role\", \"user\");\n userMessage.addProperty(\"content\", userPrompt);\n messages.add(userMessage);\n requestBody.add(\"messages\", messages);\n return requestBody;\n }\n\n private JsonObject buildGeminiGenerateContentBody(String model, String systemPrompt, String userPrompt,\n int maxTokens) {\n JsonObject requestBody = new JsonObject();\n JsonObject systemInstruction = new JsonObject();\n JsonArray systemParts = new JsonArray();\n JsonObject systemPart = new JsonObject();\n systemPart.addProperty(\"text\", systemPrompt);\n systemParts.add(systemPart);\n systemInstruction.add(\"parts\", systemParts);\n requestBody.add(\"systemInstruction\", systemInstruction);\n\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n content.addProperty(\"role\", \"user\");\n JsonArray parts = new JsonArray();\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", userPrompt);\n parts.add(part);\n content.add(\"parts\", parts);\n contents.add(content);\n requestBody.add(\"contents\", contents);\n\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0.0);\n int outputTokens = Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n outputTokens = Math.max(outputTokens, 2048);\n }\n generationConfig.addProperty(\"maxOutputTokens\", Math.min(outputTokens, MAX_TRANSLATION_OUTPUT_TOKENS));\n JsonObject thinkingConfig = new JsonObject();\n if (ModConfig.DEEPSEEK_THINKING_ENABLED.get()) {\n thinkingConfig.addProperty(\"thinkingBudget\", 1024);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n } else if (model != null && model.toLowerCase(java.util.Locale.ROOT).contains(\"flash\")) {\n thinkingConfig.addProperty(\"thinkingBudget\", 0);\n generationConfig.add(\"thinkingConfig\", thinkingConfig);\n }\n requestBody.add(\"generationConfig\", generationConfig);\n return requestBody;\n }\n\n private void applyAuthHeader(HttpRequest.Builder builder, ApiRequestProfile profile, String apiKey) {\n if (apiKey == null || apiKey.isBlank()) {\n return;\n }\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", apiKey);\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + apiKey);\n }\n }\n\n private String withApiKeyQuery(ApiRequestProfile profile, String apiKey) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || apiKey == null || apiKey.isBlank()\n || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(apiKey, StandardCharsets.UTF_8);\n }\n\n private String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return null;\n }\n\n if (response.has(\"choices\") && response.get(\"choices\").isJsonArray()) {\n JsonArray choices = response.getAsJsonArray(\"choices\");\n if (!choices.isEmpty() && choices.get(0).isJsonObject()) {\n JsonObject choice = choices.get(0).getAsJsonObject();\n if (choice.has(\"message\") && choice.get(\"message\").isJsonObject()) {\n JsonObject message = choice.getAsJsonObject(\"message\");\n if (message.has(\"content\") && !message.get(\"content\").isJsonNull()) {\n return message.get(\"content\").getAsString();\n }\n }\n if (choice.has(\"delta\") && choice.get(\"delta\").isJsonObject()) {\n JsonObject delta = choice.getAsJsonObject(\"delta\");\n if (delta.has(\"content\") && !delta.get(\"content\").isJsonNull()) {\n return delta.get(\"content\").getAsString();\n }\n }\n }\n }\n\n if (response.has(\"output_text\") && !response.get(\"output_text\").isJsonNull()) {\n return response.get(\"output_text\").getAsString();\n }\n\n if (response.has(\"output\") && response.get(\"output\").isJsonArray()) {\n JsonArray output = response.getAsJsonArray(\"output\");\n for (var element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject outputItem = element.getAsJsonObject();\n if (outputItem.has(\"content\") && outputItem.get(\"content\").isJsonArray()) {\n JsonArray content = outputItem.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n }\n }\n\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n JsonArray content = response.getAsJsonArray(\"content\");\n for (var contentPart : content) {\n if (!contentPart.isJsonObject()) {\n continue;\n }\n JsonObject part = contentPart.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n return part.get(\"text\").getAsString();\n }\n }\n }\n\n if (response.has(\"candidates\") && response.get(\"candidates\").isJsonArray()) {\n JsonArray candidates = response.getAsJsonArray(\"candidates\");\n if (!candidates.isEmpty() && candidates.get(0).isJsonObject()) {\n JsonObject candidate = candidates.get(0).getAsJsonObject();\n if (candidate.has(\"content\") && candidate.get(\"content\").isJsonObject()) {\n JsonObject content = candidate.getAsJsonObject(\"content\");\n if (content.has(\"parts\") && content.get(\"parts\").isJsonArray()) {\n JsonArray parts = content.getAsJsonArray(\"parts\");\n StringBuilder builder = new StringBuilder();\n for (var partElement : parts) {\n if (partElement.isJsonObject()) {\n JsonObject part = partElement.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n builder.append(part.get(\"text\").getAsString());\n }\n }\n }\n if (!builder.isEmpty()) {\n return builder.toString();\n }\n }\n }\n }\n }\n\n return null;\n }\n\n private record ParsedTranslationResponse(String text, TokenUsage usage) {\n private static ParsedTranslationResponse empty() {\n return new ParsedTranslationResponse(null, null);\n }\n }\n\n private record ApiRequestProfile(String endpointBase, String endpointUrl, EndpointKind kind, boolean supportsThinking) {\n private String profileKey() {\n return endpointBase + \"|\" + endpointUrl + \"|\" + kind + \"|\" + supportsThinking;\n }\n }\n\n private static String readErrorBody(InputStream body) {\n if (body == null) {\n return \"\";\n }\n try (body) {\n return new String(body.readAllBytes(), StandardCharsets.UTF_8);\n } catch (IOException e) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String value) {\n if (value == null) {\n return \"\";\n }\n String key = value.trim();\n if (key.regionMatches(true, 0, \"Bearer \", 0, 7)) {\n key = key.substring(7).trim();\n }\n if (key.length() >= 2 && key.startsWith(\"\\\"\") && key.endsWith(\"\\\"\")) {\n key = key.substring(1, key.length() - 1).trim();\n }\n return key;\n }\n\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-Forge-1.12.2/src/test/java/com/yourname/simpletranslate/validation/PortLogicValidation.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1785746130.964142, "n": 16633, "h": "4b4be68e3d1452c1", "c": "package com.yourname.simpletranslate.validation;\n\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonElement;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.sun.net.httpserver.HttpExchange;\nimport com.sun.net.httpserver.HttpHandler;\nimport com.sun.net.httpserver.HttpServer;\nimport com.yourname.simpletranslate.api.TranslationRequest;\nimport com.yourname.simpletranslate.api.TranslationResult;\nimport com.yourname.simpletranslate.chat.ChatTranslationController;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.translation.TranslationEngine;\nimport com.yourname.simpletranslate.transport.TranslationManager;\nimport com.yourname.simpletranslate.gui.SettingsButtonValidation;\nimport com.yourname.simpletranslate.gui.GuiLayoutProgramRendererValidation;\nimport com.yourname.simpletranslate.translation.TranslationQueueValidation;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.File;\nimport java.io.InputStream;\nimport java.io.OutputStream;\nimport java.net.InetSocketAddress;\nimport java.lang.reflect.Field;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.Map;\nimport java.util.concurrent.TimeUnit;\n\n/** Build-only transport fixture; it never starts Minecraft. */\npublic final class PortLogicValidation {\n private PortLogicValidation() { }\n\n public static void main(String[] args) throws Exception {\n Path root = Files.createTempDirectory(\"simple-translate-1122-validation-\");\n HttpServer server = HttpServer.create(new InetSocketAddress(\"127.0.0.1\", 0), 0);\n server.createContext(\"/\", new EchoComponentHandler());\n server.start();\n TranslationEngine engine = null;\n try {\n ModConfig.init(root.resolve(\"modern\").toFile().toPath());\n SettingsButtonValidation.run();\n GuiLayoutProgramRendererValidation.run();\n TranslationQueueValidation.run();\n validateChatDeletionIds();\n ModConfig.API_FORMAT.set(ModConfig.ApiFormat.DEEPSEEK_CHAT);\n engine = new TranslationEngine(new File(root.toFile(), \"simple_translate.properties\"));\n\n engine.updateConfiguration(\"https://api.deepseek.com\", \"fixture-key\", \"deepseek-v4-flash\",\n \"auto\", \"zh_cn\");\n require(\"https://api.deepseek.com/chat/completions\".equals(engine.getEndpoint()),\n \"DeepSeek base URL was not completed\");\n\n String endpoint = \"http://127.0.0.1:\" + server.getAddress().getPort() + \"/chat/completions\";\n engine.updateConfiguration(endpoint, \"fixture-key\", \"deepseek-v4-flash\", \"auto\", \"zh_cn\");\n require(engine.verifyApiAccess().get(15, TimeUnit.SECONDS).isAvailable(),\n \"normal Component-JSON API probe failed\");\n\n TranslationManager manager = new TranslationManager(engine);\n String managerResponse = manager.translateComponentJson(\n \"[{\\\"text\\\":\\\"First\\\"},{\\\"text\\\":\\\"Second\\\"}]\", \"book.pages.direct\", 1,\n \"auto\", \"zh_cn\", \"two ordered book pages\").get(15, TimeUnit.SECONDS);\n require(managerResponse != null\n && new JsonParser().parse(managerResponse).getAsJsonArray().size() == 2,\n \"TranslationManager nested the top-level Component array\");\n\n String fencedProviderText = engine.translateRawComponentDocument(\n \"[{\\\"text\\\":\\\"force-fenced\\\"}]\", \"chat.auto.direct\", 1,\n \"auto\", \"zh_cn\", \"{}\", Collections.emptyList())\n .get(15, TimeUnit.SECONDS);\n require(fencedProviderText != null && fencedProviderText.startsWith(\"```json\"),\n \"raw provider path consumed structural validation owned by the passthrough pipeline\");\n\n String hover = \"{\\\"text\\\":\\\"Visible\\\",\\\"hoverEvent\\\":{\\\"action\\\":\\\"show_text\\\",\\\"value\\\":{\\\"text\\\":\\\"Hidden\\\"}}}\";\n TranslationRequest valid = new TranslationRequest(\"chat.fixture\",\n Arrays.asList(hover, \"{\\\"text\\\":\\\"Second\\\"}\"),\n Collections.emptyList(), 1, \"auto\", \"zh_cn\", \"fixture\");\n TranslationResult translated = engine.translate(valid).get(15, TimeUnit.SECONDS);\n require(translated instanceof TranslationResult.Success, \"valid Component array was rejected\");\n JsonArray accepted = new JsonParser().parse(\n ((TranslationResult.Success) translated).payload()).getAsJsonArray();\n require(accepted.size() == 2, \"accepted array count changed\");\n require(accepted.get(0).getAsJsonObject().has(\"hoverEvent\"),\n \"hidden hover payload was not reattached\");\n\n exerciseFormat(engine, endpoint, ModConfig.ApiFormat.OPENAI_CHAT_COMPAT, \"gpt-4o-mini\");\n exerciseFormat(engine, \"http://127.0.0.1:\" + server.getAddress().getPort(),\n ModConfig.ApiFormat.OPENAI_RESPONSES, \"gpt-5-mini\");\n exerciseFormat(engine, \"http://127.0.0.1:\" + server.getAddress().getPort(),\n ModConfig.ApiFormat.ANTHROPIC_MESSAGES, \"claude-fixture\");\n exerciseFormat(engine, \"http://127.0.0.1:\" + server.getAddress().getPort(),\n ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT, \"gemini-flash-fixture\");\n exerciseFormat(engine, \"http://127.0.0.1:\" + server.getAddress().getPort(),\n ModConfig.ApiFormat.LOCAL_OLLAMA, \"qwen-fixture\");\n\n ModConfig.API_FORMAT.set(ModConfig.ApiFormat.DEEPSEEK_CHAT);\n engine.updateConfiguration(endpoint, \"fixture-key\", \"deepseek-v4-flash\", \"auto\", \"zh_cn\");\n\n TranslationRequest mismatch = new TranslationRequest(\"chat.fixture.mismatch\",\n Collections.singletonList(\"{\\\"text\\\":\\\"force-mismatch\\\"}\"), \"auto\", \"zh_cn\");\n TranslationResult rejected = engine.translate(mismatch).get(20, TimeUnit.SECONDS);\n require(rejected instanceof TranslationResult.Failed,\n \"mismatched top-level response count was accepted\");\n System.out.println(\"PORT_LOGIC_VALIDATION_OK\");\n } finally {\n if (engine != null) engine.shutdown();\n server.stop(0);\n deleteTree(root.toFile());\n }\n }\n\n /** Build-only regression for mod-supplied replacement IDs and stale chat requests. */\n private static void validateChatDeletionIds() throws Exception {\n ChatTranslationController.clearRuntimeState();\n require(ChatTranslationController.retain(\n new net.minecraft.util.text.TextComponentString(\"first\"), 31415) == 31415,\n \"AUTO chat discarded the caller deletion ID\");\n require(ChatTranslationController.retain(\n new net.minecraft.util.text.TextComponentString(\"newer\"), 31415) == 31415,\n \"AUTO chat changed a reused caller deletion ID\");\n require(privateMapSize(\"PENDING\") == 1 && privateMapSize(\"PENDING_BY_DISPLAY_ID\") == 1,\n \"AUTO chat kept a stale request for a reused deletion ID\");\n\n int generatedA = ChatTranslationController.retain(\n new net.minecraft.util.text.TextComponentString(\"generated-a\"), 0);\n int generatedB = ChatTranslationController.retain(\n new net.minecraft.util.text.TextComponentString(\"generated-b\"), 0);\n require(generatedA != 0 && generatedB != 0 && generatedA != generatedB,\n \"AUTO chat generated an invalid or duplicate internal deletion ID\");\n\n ChatTranslationController.clearRuntimeState();\n ChatTranslationController.ButtonPresentation first = ChatTranslationController.attachButton(\n new net.minecraft.util.text.TextComponentString(\"button-first\"), 27182);\n ChatTranslationController.ButtonPresentation newer = ChatTranslationController.attachButton(\n new net.minecraft.util.text.TextComponentString(\"button-newer\"), 27182);\n require(first.id == 27182 && newer.id == 27182 && privateMapSize(\"BUTTONS\") == 1,\n \"button chat did not replace the previous entry for a caller deletion ID\");\n ChatTranslationController.ButtonPresentation generated = ChatTranslationController.attachButton(\n new net.minecraft.util.text.TextComponentString(\"button-generated\"), 0);\n require(generated.id != 0 && generated.id != 27182,\n \"button chat generated an invalid internal deletion ID\");\n\n ChatTranslationController.clearRuntimeState();\n ChatTranslationController.retain(\n new net.minecraft.util.text.TextComponentString(\"replace-me\"), 99);\n ChatTranslationController.attachButton(\n new net.minecraft.util.text.TextComponentString(\"replace-button\"), 99);\n ChatTranslationController.invalidateExternalReplacement(99);\n require(privateMapSize(\"PENDING_BY_DISPLAY_ID\") == 0\n && privateMapSize(\"PENDING\") == 0\n && privateMapSize(\"BUTTONS\") == 0,\n \"external replacement did not invalidate older chat work\");\n ChatTranslationController.clearRuntimeState();\n }\n\n private static int privateMapSize(String fieldName) throws Exception {\n Field field = ChatTranslationController.class.getDeclaredField(fieldName);\n field.setAccessible(true);\n return ((Map) field.get(null)).size();\n }\n\n private static void require(boolean condition, String message) {\n if (!condition) throw new IllegalStateException(message);\n }\n\n private static void exerciseFormat(TranslationEngine engine, String endpoint,\n ModConfig.ApiFormat format, String model) throws Exception {\n ModConfig.API_FORMAT.set(format);\n engine.updateConfiguration(endpoint, \"fixture-key\", model, \"auto\", \"zh_cn\");\n TranslationRequest request = new TranslationRequest(\"chat.fixture.\" + format.name().toLowerCase(),\n Collections.singletonList(\"{\\\"text\\\":\\\"Provider \" + format.name() + \"\\\"}\"),\n \"auto\", \"zh_cn\");\n TranslationResult result = engine.translate(request).get(15, TimeUnit.SECONDS);\n require(result instanceof TranslationResult.Success, format + \" provider fixture failed\");\n }\n\n private static void deleteTree(File file) {\n if (file == null || !file.exists()) return;\n File[] children = file.listFiles();\n if (children != null) for (File child : children) deleteTree(child);\n if (!file.delete()) file.deleteOnExit();\n }\n\n private static final class EchoComponentHandler implements HttpHandler {\n @Override public void handle(HttpExchange exchange) {\n try {\n String path = exchange.getRequestURI().getPath();\n boolean anthropic = path.endsWith(\"/messages\");\n boolean gemini = path.contains(\":generateContent\");\n boolean ollama = path.endsWith(\"/chat/completions\")\n && exchange.getRequestHeaders().getFirst(\"Authorization\") == null;\n if (anthropic) {\n require(\"fixture-key\".equals(exchange.getRequestHeaders().getFirst(\"x-api-key\")),\n \"Anthropic API key missing\");\n } else if (gemini) {\n require(exchange.getRequestURI().getQuery() != null\n && exchange.getRequestURI().getQuery().contains(\"key=fixture-key\"),\n \"Gemini query API key missing\");\n } else if (!ollama) {\n require(\"Bearer fixture-key\".equals(exchange.getRequestHeaders().getFirst(\"Authorization\")),\n \"Bearer authorization missing\");\n }\n JsonObject request = new JsonParser().parse(read(exchange.getRequestBody())).getAsJsonObject();\n String document;\n if (path.endsWith(\"/responses\")) {\n document = request.getAsJsonArray(\"input\").get(0).getAsJsonObject()\n .getAsJsonArray(\"content\").get(0).getAsJsonObject().get(\"text\").getAsString();\n } else if (gemini) {\n document = request.getAsJsonArray(\"contents\").get(0).getAsJsonObject()\n .getAsJsonArray(\"parts\").get(0).getAsJsonObject().get(\"text\").getAsString();\n } else {\n JsonArray messages = request.getAsJsonArray(\"messages\");\n document = messages.get(messages.size() - 1).getAsJsonObject().get(\"content\").getAsString();\n }\n JsonElement parsed = new JsonParser().parse(document);\n require(parsed.isJsonArray(), \"wire request was not a top-level Component array\");\n require(parsed.getAsJsonArray().size() == 0\n || !parsed.getAsJsonArray().get(0).isJsonArray(),\n \"wire request nested the top-level Component array\");\n require(!document.contains(\"Hidden\"), \"ordinary request leaked hidden hover text\");\n String content = document.contains(\"force-mismatch\") ? \"[]\" : document;\n if (document.contains(\"force-fenced\")) content = \"```json\\n\" + content + \"\\n```\";\n JsonObject response = new JsonObject();\n if (path.endsWith(\"/responses\")) {\n response.addProperty(\"output_text\", content);\n } else if (anthropic) {\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", content);\n JsonArray parts = new JsonArray();\n parts.add(part);\n response.add(\"content\", parts);\n } else if (gemini) {\n JsonObject part = new JsonObject();\n part.addProperty(\"text\", content);\n JsonArray parts = new JsonArray();\n parts.add(part);\n JsonObject candidateContent = new JsonObject();\n candidateContent.add(\"parts\", parts);\n JsonObject candidate = new JsonObject();\n candidate.add(\"content\", candidateContent);\n JsonArray candidates = new JsonArray();\n candidates.add(candidate);\n response.add(\"candidates\", candidates);\n } else {\n JsonObject message = new JsonObject();\n message.addProperty(\"content\", content);\n JsonObject choice = new JsonObject();\n choice.add(\"message\", message);\n JsonArray choices = new JsonArray();\n choices.add(choice);\n response.add(\"choices\", choices);\n }\n JsonObject usage = new JsonObject();\n usage.addProperty(\"prompt_tokens\", 7);\n usage.addProperty(\"completion_tokens\", 5);\n usage.addProperty(\"total_tokens\", 12);\n response.add(\"usage\", usage);\n byte[] bytes = response.toString().getBytes(StandardCharsets.UTF_8);\n exchange.getResponseHeaders().set(\"Content-Type\", \"application/json; charset=utf-8\");\n exchange.sendResponseHeaders(200, bytes.length);\n OutputStream output = exchange.getResponseBody();\n output.write(bytes);\n output.close();\n } catch (Throwable error) {\n try {\n byte[] bytes = error.toString().getBytes(StandardCharsets.UTF_8);\n exchange.sendResponseHeaders(500, bytes.length);\n OutputStream output = exchange.getResponseBody();\n output.write(bytes);\n output.close();\n } catch (Exception ignored) { }\n } finally {\n exchange.close();\n }\n }\n\n private static String read(InputStream input) throws Exception {\n ByteArrayOutputStream output = new ByteArrayOutputStream();\n byte[] buffer = new byte[4096];\n int count;\n while ((count = input.read(buffer)) >= 0) output.write(buffer, 0, count);\n return new String(output.toByteArray(), StandardCharsets.UTF_8);\n }\n }\n}\n"}}}}