{"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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067433.3359134, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067442.2073267, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067443.2531703, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067439.559364, "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"}}, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067431.5283718, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067437.6029313, "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"}}, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067431.298133, "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"}}, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067439.5526502, "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"}}, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067435.966007, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067431.6207056, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067439.8341432, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067431.3242574, "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"}}, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067442.4673622, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067439.5887384, "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"}}, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067442.4911997, "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"}}, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067436.1320732, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067439.2195108, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067437.3502152, "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"}}, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067437.6724882, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067442.303737, "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"}}, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067439.4883695, "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"}}, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067439.7984996, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067433.2437146, "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"}}, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067431.3928478, "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.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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067442.1637514, "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"}}, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067439.5413525, "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"}}, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067439.8922276, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067433.5963538, "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"}, "6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067438.137801, "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"}}, "fabric/SimpleTranslate-Fabric-1.19.2/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067431.0148797, "n": 12577, "h": "e27eb3042a91b4bf", "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 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 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 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 = ButtonCompat.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\n\n"}}, "fabric/SimpleTranslate-Fabric-1.21.4/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067431.354005, "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"}, "c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068303.8566563, "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.8/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067431.3702738, "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.4/src/main/java/com/yourname/simpletranslate/translation/DeepSeekTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067432.1471212, "n": 60896, "h": "4db9c81e047177fe", "c": "package com.yourname.simpletranslate.translation;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.util.ModelOutputSanitizer;\nimport com.yourname.simpletranslate.util.TranslationCacheKeys;\nimport com.yourname.simpletranslate.util.TranslationMapping;\nimport com.yourname.simpletranslate.util.TranslationMappingResult;\nimport com.yourname.simpletranslate.util.TranslationSlot;\nimport com.yourname.simpletranslate.util.TranslationTextDetector;\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.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 private static final Pattern ALLOWED_MAPPING_PLACEHOLDER_PATTERN = Pattern.compile(\"@@@F\\\\d+@@@\");\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 = 8192;\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 DIRECT_SURFACE_PATTERN = Pattern.compile(\"]*\\\\bsurface=\\\"([^\\\"]+)\\\"\");\n private static final Pattern MAPPING_SURFACE_PATTERN = Pattern.compile(\"\\\"surface\\\"\\\\s*:\\\\s*\\\"([^\\\"]+)\\\"\");\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 /** Alias so existing references keep working after the prompt-builder split. */\n private static final class RequestMode {\n static final TranslationPrompts.RequestMode PLAIN_TEXT = TranslationPrompts.RequestMode.PLAIN_TEXT;\n static final TranslationPrompts.RequestMode PRESERVE_MARKERS = TranslationPrompts.RequestMode.PRESERVE_MARKERS;\n static final TranslationPrompts.RequestMode MAPPING_JSON = TranslationPrompts.RequestMode.MAPPING_JSON;\n static final TranslationPrompts.RequestMode DIRECT_FORMATTED = TranslationPrompts.RequestMode.DIRECT_FORMATTED;\n\n private RequestMode() {\n }\n }\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 @Override\n public CompletableFuture translate(String text, String sourceLanguage, String targetLanguage) {\n return translateWithTerms(text, sourceLanguage, targetLanguage, List.of());\n }\n\n @Override\n public CompletableFuture translateWithTerms(String text, String sourceLanguage, String targetLanguage,\n List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.PLAIN_TEXT);\n return sendRequest(systemPrompt, text);\n }\n\n @Override\n public CompletableFuture translatePreservingMarkers(String text, String sourceLanguage,\n String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.PRESERVE_MARKERS);\n return sendRequest(systemPrompt, text);\n }\n\n @Override\n public CompletableFuture translateFormattedDocument(String document, String surface,\n String sourceLanguage, String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n // Style/term hints live in the user payload so the system prompt stays\n // byte-stable per language pair (provider prefix caching).\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, List.of(),\n RequestMode.DIRECT_FORMATTED);\n String userPrompt = insertDirectSections(document, TranslationPrompts.directUserSections(termHints));\n return sendRequest(systemPrompt, userPrompt, estimateDirectMaxTokens(document), surface);\n }\n\n /** Same as {@link #translateFormattedDocument} but scales the output token budget. */\n public CompletableFuture translateFormattedDocument(String document, String surface,\n String sourceLanguage, String targetLanguage, List termHints, int maxTokenMultiplier) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, List.of(),\n RequestMode.DIRECT_FORMATTED);\n String userPrompt = insertDirectSections(document, TranslationPrompts.directUserSections(termHints));\n return sendRequest(systemPrompt, userPrompt, estimateDirectMaxTokens(document, maxTokenMultiplier), surface);\n }\n\n /** Inserts [STYLE]/[TERMS] sections right before the [TEXT] block. */\n private static String insertDirectSections(String document, String sections) {\n if (sections == null || sections.isBlank() || document == null) {\n return document;\n }\n int index = document.indexOf(\"[TEXT\");\n if (index < 0) {\n return sections + document;\n }\n return document.substring(0, index) + sections + document.substring(index);\n }\n\n /** Output budget follows the [TEXT] block size, not the whole payload with context. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Output budget follows the [TEXT] block; multiplier supports partial retries. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int start = body.indexOf(\"[TEXT\");\n int end = body.lastIndexOf(\"[/TEXT]\");\n if (start >= 0 && end > start) {\n body = body.substring(start, end);\n }\n int lineCount = 0;\n for (String line : body.split(\"\\\\R\")) {\n if (line != null && line.matches(\"(?s).*?\\\\d{1,4}\\\\s*[||].*\")) {\n lineCount++;\n }\n }\n // Chinese game text often expands; multi-line tooltips need headroom per line.\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int lineEstimate = lineCount * 192 + 384;\n int estimated = Math.max(charEstimate, lineEstimate);\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 @Override\n public CompletableFuture translateMapping(TranslationMapping mapping,\n String sourceLanguage, String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.MAPPING_JSON);\n String userPrompt = buildMappingUserPrompt(mapping, sourceLanguage, targetLanguage);\n return sendRequest(systemPrompt, userPrompt).thenApply(raw -> parseMappingResponse(raw, mapping));\n }\n\n @Override\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationService.ApiDetectionResult(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 @Override\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationService.ModelDetectionResult(\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 @Override\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 TranslationService.ModelAccessResult(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationService.ModelAccessResult(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationService.ModelAccessResult(\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 String buildSystemPrompt(String sourceLanguage, String targetLanguage, List termHints,\n TranslationPrompts.RequestMode mode) {\n return TranslationPrompts.buildSystemPrompt(sourceLanguage, targetLanguage, termHints, mode);\n }\n\n\n private String buildMappingUserPrompt(TranslationMapping mapping, String sourceLanguage, String targetLanguage) {\n JsonObject root = new JsonObject();\n root.addProperty(\"version\", \"mapping-v1\");\n root.addProperty(\"jobId\", mapping.taskId());\n root.addProperty(\"surface\", mapping.surface());\n root.addProperty(\"sourceLanguage\", getLanguageName(sourceLanguage));\n root.addProperty(\"targetLanguage\", getLanguageName(targetLanguage));\n root.addProperty(\"context\", mapping.context());\n root.addProperty(\"layoutSignature\", mapping.layoutSignature());\n root.addProperty(\"styleSignature\", mapping.styleSignature());\n boolean flexibleLayout = isFlexibleMappingSurface(mapping.surface());\n root.addProperty(\"layoutMode\", flexibleLayout ? \"flexible-tooltip\" : \"fixed-layout\");\n root.addProperty(\"instructions\", flexibleLayout\n ? \"Translate every unit as its own semantic style block or styled token. Use natural Minecraft localization for the target language, ignore source visual line breaks, keep every @@@F marker exactly once in the same unit, and preserve only structural tokens such as numbers, ids, commands, control codes, and placeholders.\"\n : \"Translate each fixed-layout slot conservatively. Preserve line/slot structure, technical tokens, and every @@@F marker exactly.\");\n\n JsonArray units = new JsonArray();\n for (TranslationSlot slot : mapping.slots()) {\n JsonObject unit = new JsonObject();\n unit.addProperty(\"id\", slot.id());\n unit.addProperty(\"text\", slot.text());\n unit.addProperty(\"role\", slot.role());\n unit.addProperty(\"maxChars\", slot.maxWidth());\n unit.addProperty(\"fixed\", slot.fixedSlot());\n unit.addProperty(\"lineIndex\", slot.lineIndex());\n unit.addProperty(\"slotIndex\", slot.slotIndex());\n unit.addProperty(\"styleSignature\", slot.styleSignature());\n unit.addProperty(\"tokenMask\", slot.tokenMask());\n units.add(unit);\n }\n root.add(\"units\", units);\n return root.toString();\n }\n\n private boolean isFlexibleMappingSurface(String surface) {\n if (surface == null) {\n return false;\n }\n String normalized = surface.toLowerCase();\n return normalized.startsWith(\"tooltip.\")\n || normalized.startsWith(\"hover.\")\n || normalized.startsWith(\"book.\");\n }\n\n private String getLanguageName(String code) {\n return TranslationTextDetector.displayLanguageName(code);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt) {\n return sendRequest(systemPrompt, userPrompt, estimateMaxTokens(userPrompt));\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens) {\n return sendRequest(systemPrompt, userPrompt, maxTokens, null);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String explicitSurface) {\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 surface = explicitSurface == null || explicitSurface.isBlank()\n ? detectRequestSurface(userPrompt)\n : explicitSurface;\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestBlocking(profiles, apiKey, model, systemPrompt, userPrompt, maxTokens));\n }\n\n private String sendRequestBlocking(List profiles, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n for (ApiRequestProfile profile : orderedProfiles) {\n String translated = sendRequestWithProfile(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return translated;\n }\n }\n return null;\n }\n\n private String sendRequestWithProfile(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return null;\n }\n\n long startedAt = System.nanoTime();\n try {\n HttpRequest request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n String result = parseResponse(response.body(), startedAt);\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 return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\"Empty translation result from DeepSeek\");\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 (InterruptedException e) {\n Thread.currentThread().interrupt();\n SimpleTranslateMod.getLogger().error(\"Translation request interrupted\", e);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n throw e;\n } catch (InvalidApiResponseException e) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\", e.getMessage());\n return null;\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\", e.getMessage(), e);\n return null;\n }\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 String detectRequestSurface(String userPrompt) {\n if (userPrompt == null || userPrompt.isBlank()) {\n return \"manager.raw\";\n }\n Matcher direct = DIRECT_SURFACE_PATTERN.matcher(userPrompt);\n if (direct.find()) {\n return direct.group(1);\n }\n Matcher mapping = MAPPING_SURFACE_PATTERN.matcher(userPrompt);\n if (mapping.find()) {\n return mapping.group(1);\n }\n return \"manager.raw\";\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 static int estimateMaxTokens(String text) {\n int length = text == null ? 0 : text.length();\n int estimated = length + 512;\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n return MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n return Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.contains(\"data:\")) {\n return parseStreamingResponse(responseBody);\n }\n return parsePlainResponse(responseBody);\n }\n\n private String parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return null;\n }\n\n Future readerTask = 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 null;\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 String parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return null;\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\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 null;\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);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return null;\n }\n\n if (sawData) {\n return ModelOutputSanitizer.sanitize(result.toString());\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 String parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\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);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return null;\n }\n return ModelOutputSanitizer.sanitize(result.toString());\n }\n\n private String parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\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 ModelOutputSanitizer.sanitize(raw);\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 TranslationMappingResult parseMappingResponse(String response, TranslationMapping mapping) {\n String sanitized = ModelOutputSanitizer.sanitize(response);\n if (sanitized == null || sanitized.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n try {\n String jsonPayload = extractJsonObjectPayload(sanitized);\n if (jsonPayload == null || jsonPayload.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n JsonObject root = JsonParser.parseString(jsonPayload).getAsJsonObject();\n String version = root.has(\"version\") ? root.get(\"version\").getAsString() : \"\";\n String jobId = root.has(\"jobId\") ? root.get(\"jobId\").getAsString() : \"\";\n if (!\"mapping-v1\".equals(version) || !mapping.taskId().equals(jobId)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n if (!root.has(\"translations\") || !root.get(\"translations\").isJsonArray()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n Map translations = new LinkedHashMap<>();\n for (var element : root.getAsJsonArray(\"translations\")) {\n if (!element.isJsonObject()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n JsonObject item = element.getAsJsonObject();\n String id = item.has(\"id\") ? item.get(\"id\").getAsString() : \"\";\n String translation = item.has(\"translation\") ? item.get(\"translation\").getAsString() : \"\";\n if (id.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n String cleaned = ModelOutputSanitizer.sanitize(translation);\n if (cleaned == null || cleaned.isBlank() || containsDisallowedMappingTokens(cleaned)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n translations.put(id, cleaned);\n }\n\n if (translations.size() != mapping.slots().size()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n for (TranslationSlot slot : mapping.slots()) {\n if (!translations.containsKey(slot.id())) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n if (slot.fixedSlot()) {\n String normalizedSource = TranslationCacheKeys.normalizeSource(slot.text());\n String normalizedTranslated = TranslationCacheKeys.normalizeSource(translations.get(slot.id()));\n if (!normalizedSource.equals(normalizedTranslated)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n }\n }\n\n return new TranslationMappingResult(mapping.taskId(), translations);\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().debug(\"Failed to parse mapping response\", e);\n return new TranslationMappingResult(mapping.taskId(), Map.of());\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 boolean containsDisallowedMappingTokens(String text) {\n if (text == null || text.isEmpty()) {\n return false;\n }\n return containsDisallowedMappingMarker(text)\n || text.contains(\"```\")\n || text.contains(\"\")\n || text.contains(\"\")\n || text.contains(\"\")\n || text.contains(\"\");\n }\n\n private boolean containsDisallowedMappingMarker(String text) {\n int index = text.indexOf(\"@@@\");\n if (index < 0) {\n return false;\n }\n Matcher matcher = ALLOWED_MAPPING_PLACEHOLDER_PATTERN.matcher(text);\n int cursor = 0;\n while (matcher.find()) {\n String between = text.substring(cursor, matcher.start());\n if (between.contains(\"@@@\")) {\n return true;\n }\n cursor = matcher.end();\n }\n return text.substring(cursor).contains(\"@@@\");\n }\n\n private void appendStreamingChunk(StringBuilder result, String json) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\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 TranslationService.ApiDetectionResult 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 = sendRequestWithProfile(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationService.ApiDetectionResult(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 TranslationService.ApiDetectionResult(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationService.ModelAccessResult 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 = buildSystemPrompt(\"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(),\n RequestMode.DIRECT_FORMATTED);\n String probeUserPrompt = \"\"\"\n \n [TEXT lines=1]\n 0|Hello\n [/TEXT]\n \n \"\"\";\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.send(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream());\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 TranslationService.ModelAccessResult(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 (InterruptedException e) {\n Thread.currentThread().interrupt();\n return new TranslationService.ModelAccessResult(false, modelId, 0, \"Model verification interrupted\");\n } catch (IOException | RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationService.ModelAccessResult(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n for (String line : value.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (trimmed.matches(\"0\\\\s*[||].+\")) {\n return true;\n }\n }\n return false;\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, false));\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 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(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 requestBody.addProperty(\"max_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n if (!thinkingEnabled) {\n requestBody.addProperty(\"temperature\", 0.1);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\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 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 requestBody.addProperty(\"max_output_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n requestBody.addProperty(\"temperature\", 0.1);\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 requestBody.addProperty(\"max_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n requestBody.addProperty(\"temperature\", 0.1);\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 systemPrompt, String userPrompt, 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.1);\n generationConfig.addProperty(\"maxOutputTokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\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 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 @Override\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n @Override\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.10/src/main/java/com/yourname/simpletranslate/translation/DeepSeekTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067433.4950025, "n": 60896, "h": "4db9c81e047177fe", "c": "package com.yourname.simpletranslate.translation;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.util.ModelOutputSanitizer;\nimport com.yourname.simpletranslate.util.TranslationCacheKeys;\nimport com.yourname.simpletranslate.util.TranslationMapping;\nimport com.yourname.simpletranslate.util.TranslationMappingResult;\nimport com.yourname.simpletranslate.util.TranslationSlot;\nimport com.yourname.simpletranslate.util.TranslationTextDetector;\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.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 private static final Pattern ALLOWED_MAPPING_PLACEHOLDER_PATTERN = Pattern.compile(\"@@@F\\\\d+@@@\");\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 = 8192;\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 DIRECT_SURFACE_PATTERN = Pattern.compile(\"]*\\\\bsurface=\\\"([^\\\"]+)\\\"\");\n private static final Pattern MAPPING_SURFACE_PATTERN = Pattern.compile(\"\\\"surface\\\"\\\\s*:\\\\s*\\\"([^\\\"]+)\\\"\");\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 /** Alias so existing references keep working after the prompt-builder split. */\n private static final class RequestMode {\n static final TranslationPrompts.RequestMode PLAIN_TEXT = TranslationPrompts.RequestMode.PLAIN_TEXT;\n static final TranslationPrompts.RequestMode PRESERVE_MARKERS = TranslationPrompts.RequestMode.PRESERVE_MARKERS;\n static final TranslationPrompts.RequestMode MAPPING_JSON = TranslationPrompts.RequestMode.MAPPING_JSON;\n static final TranslationPrompts.RequestMode DIRECT_FORMATTED = TranslationPrompts.RequestMode.DIRECT_FORMATTED;\n\n private RequestMode() {\n }\n }\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 @Override\n public CompletableFuture translate(String text, String sourceLanguage, String targetLanguage) {\n return translateWithTerms(text, sourceLanguage, targetLanguage, List.of());\n }\n\n @Override\n public CompletableFuture translateWithTerms(String text, String sourceLanguage, String targetLanguage,\n List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.PLAIN_TEXT);\n return sendRequest(systemPrompt, text);\n }\n\n @Override\n public CompletableFuture translatePreservingMarkers(String text, String sourceLanguage,\n String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.PRESERVE_MARKERS);\n return sendRequest(systemPrompt, text);\n }\n\n @Override\n public CompletableFuture translateFormattedDocument(String document, String surface,\n String sourceLanguage, String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n // Style/term hints live in the user payload so the system prompt stays\n // byte-stable per language pair (provider prefix caching).\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, List.of(),\n RequestMode.DIRECT_FORMATTED);\n String userPrompt = insertDirectSections(document, TranslationPrompts.directUserSections(termHints));\n return sendRequest(systemPrompt, userPrompt, estimateDirectMaxTokens(document), surface);\n }\n\n /** Same as {@link #translateFormattedDocument} but scales the output token budget. */\n public CompletableFuture translateFormattedDocument(String document, String surface,\n String sourceLanguage, String targetLanguage, List termHints, int maxTokenMultiplier) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, List.of(),\n RequestMode.DIRECT_FORMATTED);\n String userPrompt = insertDirectSections(document, TranslationPrompts.directUserSections(termHints));\n return sendRequest(systemPrompt, userPrompt, estimateDirectMaxTokens(document, maxTokenMultiplier), surface);\n }\n\n /** Inserts [STYLE]/[TERMS] sections right before the [TEXT] block. */\n private static String insertDirectSections(String document, String sections) {\n if (sections == null || sections.isBlank() || document == null) {\n return document;\n }\n int index = document.indexOf(\"[TEXT\");\n if (index < 0) {\n return sections + document;\n }\n return document.substring(0, index) + sections + document.substring(index);\n }\n\n /** Output budget follows the [TEXT] block size, not the whole payload with context. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Output budget follows the [TEXT] block; multiplier supports partial retries. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int start = body.indexOf(\"[TEXT\");\n int end = body.lastIndexOf(\"[/TEXT]\");\n if (start >= 0 && end > start) {\n body = body.substring(start, end);\n }\n int lineCount = 0;\n for (String line : body.split(\"\\\\R\")) {\n if (line != null && line.matches(\"(?s).*?\\\\d{1,4}\\\\s*[||].*\")) {\n lineCount++;\n }\n }\n // Chinese game text often expands; multi-line tooltips need headroom per line.\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int lineEstimate = lineCount * 192 + 384;\n int estimated = Math.max(charEstimate, lineEstimate);\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 @Override\n public CompletableFuture translateMapping(TranslationMapping mapping,\n String sourceLanguage, String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.MAPPING_JSON);\n String userPrompt = buildMappingUserPrompt(mapping, sourceLanguage, targetLanguage);\n return sendRequest(systemPrompt, userPrompt).thenApply(raw -> parseMappingResponse(raw, mapping));\n }\n\n @Override\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationService.ApiDetectionResult(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 @Override\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationService.ModelDetectionResult(\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 @Override\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 TranslationService.ModelAccessResult(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationService.ModelAccessResult(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationService.ModelAccessResult(\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 String buildSystemPrompt(String sourceLanguage, String targetLanguage, List termHints,\n TranslationPrompts.RequestMode mode) {\n return TranslationPrompts.buildSystemPrompt(sourceLanguage, targetLanguage, termHints, mode);\n }\n\n\n private String buildMappingUserPrompt(TranslationMapping mapping, String sourceLanguage, String targetLanguage) {\n JsonObject root = new JsonObject();\n root.addProperty(\"version\", \"mapping-v1\");\n root.addProperty(\"jobId\", mapping.taskId());\n root.addProperty(\"surface\", mapping.surface());\n root.addProperty(\"sourceLanguage\", getLanguageName(sourceLanguage));\n root.addProperty(\"targetLanguage\", getLanguageName(targetLanguage));\n root.addProperty(\"context\", mapping.context());\n root.addProperty(\"layoutSignature\", mapping.layoutSignature());\n root.addProperty(\"styleSignature\", mapping.styleSignature());\n boolean flexibleLayout = isFlexibleMappingSurface(mapping.surface());\n root.addProperty(\"layoutMode\", flexibleLayout ? \"flexible-tooltip\" : \"fixed-layout\");\n root.addProperty(\"instructions\", flexibleLayout\n ? \"Translate every unit as its own semantic style block or styled token. Use natural Minecraft localization for the target language, ignore source visual line breaks, keep every @@@F marker exactly once in the same unit, and preserve only structural tokens such as numbers, ids, commands, control codes, and placeholders.\"\n : \"Translate each fixed-layout slot conservatively. Preserve line/slot structure, technical tokens, and every @@@F marker exactly.\");\n\n JsonArray units = new JsonArray();\n for (TranslationSlot slot : mapping.slots()) {\n JsonObject unit = new JsonObject();\n unit.addProperty(\"id\", slot.id());\n unit.addProperty(\"text\", slot.text());\n unit.addProperty(\"role\", slot.role());\n unit.addProperty(\"maxChars\", slot.maxWidth());\n unit.addProperty(\"fixed\", slot.fixedSlot());\n unit.addProperty(\"lineIndex\", slot.lineIndex());\n unit.addProperty(\"slotIndex\", slot.slotIndex());\n unit.addProperty(\"styleSignature\", slot.styleSignature());\n unit.addProperty(\"tokenMask\", slot.tokenMask());\n units.add(unit);\n }\n root.add(\"units\", units);\n return root.toString();\n }\n\n private boolean isFlexibleMappingSurface(String surface) {\n if (surface == null) {\n return false;\n }\n String normalized = surface.toLowerCase();\n return normalized.startsWith(\"tooltip.\")\n || normalized.startsWith(\"hover.\")\n || normalized.startsWith(\"book.\");\n }\n\n private String getLanguageName(String code) {\n return TranslationTextDetector.displayLanguageName(code);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt) {\n return sendRequest(systemPrompt, userPrompt, estimateMaxTokens(userPrompt));\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens) {\n return sendRequest(systemPrompt, userPrompt, maxTokens, null);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String explicitSurface) {\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 surface = explicitSurface == null || explicitSurface.isBlank()\n ? detectRequestSurface(userPrompt)\n : explicitSurface;\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestBlocking(profiles, apiKey, model, systemPrompt, userPrompt, maxTokens));\n }\n\n private String sendRequestBlocking(List profiles, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n for (ApiRequestProfile profile : orderedProfiles) {\n String translated = sendRequestWithProfile(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return translated;\n }\n }\n return null;\n }\n\n private String sendRequestWithProfile(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return null;\n }\n\n long startedAt = System.nanoTime();\n try {\n HttpRequest request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n String result = parseResponse(response.body(), startedAt);\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 return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\"Empty translation result from DeepSeek\");\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 (InterruptedException e) {\n Thread.currentThread().interrupt();\n SimpleTranslateMod.getLogger().error(\"Translation request interrupted\", e);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n throw e;\n } catch (InvalidApiResponseException e) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\", e.getMessage());\n return null;\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\", e.getMessage(), e);\n return null;\n }\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 String detectRequestSurface(String userPrompt) {\n if (userPrompt == null || userPrompt.isBlank()) {\n return \"manager.raw\";\n }\n Matcher direct = DIRECT_SURFACE_PATTERN.matcher(userPrompt);\n if (direct.find()) {\n return direct.group(1);\n }\n Matcher mapping = MAPPING_SURFACE_PATTERN.matcher(userPrompt);\n if (mapping.find()) {\n return mapping.group(1);\n }\n return \"manager.raw\";\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 static int estimateMaxTokens(String text) {\n int length = text == null ? 0 : text.length();\n int estimated = length + 512;\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n return MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n return Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.contains(\"data:\")) {\n return parseStreamingResponse(responseBody);\n }\n return parsePlainResponse(responseBody);\n }\n\n private String parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return null;\n }\n\n Future readerTask = 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 null;\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 String parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return null;\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\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 null;\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);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return null;\n }\n\n if (sawData) {\n return ModelOutputSanitizer.sanitize(result.toString());\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 String parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\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);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return null;\n }\n return ModelOutputSanitizer.sanitize(result.toString());\n }\n\n private String parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\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 ModelOutputSanitizer.sanitize(raw);\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 TranslationMappingResult parseMappingResponse(String response, TranslationMapping mapping) {\n String sanitized = ModelOutputSanitizer.sanitize(response);\n if (sanitized == null || sanitized.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n try {\n String jsonPayload = extractJsonObjectPayload(sanitized);\n if (jsonPayload == null || jsonPayload.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n JsonObject root = JsonParser.parseString(jsonPayload).getAsJsonObject();\n String version = root.has(\"version\") ? root.get(\"version\").getAsString() : \"\";\n String jobId = root.has(\"jobId\") ? root.get(\"jobId\").getAsString() : \"\";\n if (!\"mapping-v1\".equals(version) || !mapping.taskId().equals(jobId)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n if (!root.has(\"translations\") || !root.get(\"translations\").isJsonArray()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n Map translations = new LinkedHashMap<>();\n for (var element : root.getAsJsonArray(\"translations\")) {\n if (!element.isJsonObject()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n JsonObject item = element.getAsJsonObject();\n String id = item.has(\"id\") ? item.get(\"id\").getAsString() : \"\";\n String translation = item.has(\"translation\") ? item.get(\"translation\").getAsString() : \"\";\n if (id.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n String cleaned = ModelOutputSanitizer.sanitize(translation);\n if (cleaned == null || cleaned.isBlank() || containsDisallowedMappingTokens(cleaned)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n translations.put(id, cleaned);\n }\n\n if (translations.size() != mapping.slots().size()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n for (TranslationSlot slot : mapping.slots()) {\n if (!translations.containsKey(slot.id())) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n if (slot.fixedSlot()) {\n String normalizedSource = TranslationCacheKeys.normalizeSource(slot.text());\n String normalizedTranslated = TranslationCacheKeys.normalizeSource(translations.get(slot.id()));\n if (!normalizedSource.equals(normalizedTranslated)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n }\n }\n\n return new TranslationMappingResult(mapping.taskId(), translations);\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().debug(\"Failed to parse mapping response\", e);\n return new TranslationMappingResult(mapping.taskId(), Map.of());\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 boolean containsDisallowedMappingTokens(String text) {\n if (text == null || text.isEmpty()) {\n return false;\n }\n return containsDisallowedMappingMarker(text)\n || text.contains(\"```\")\n || text.contains(\"\")\n || text.contains(\"\")\n || text.contains(\"\")\n || text.contains(\"\");\n }\n\n private boolean containsDisallowedMappingMarker(String text) {\n int index = text.indexOf(\"@@@\");\n if (index < 0) {\n return false;\n }\n Matcher matcher = ALLOWED_MAPPING_PLACEHOLDER_PATTERN.matcher(text);\n int cursor = 0;\n while (matcher.find()) {\n String between = text.substring(cursor, matcher.start());\n if (between.contains(\"@@@\")) {\n return true;\n }\n cursor = matcher.end();\n }\n return text.substring(cursor).contains(\"@@@\");\n }\n\n private void appendStreamingChunk(StringBuilder result, String json) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\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 TranslationService.ApiDetectionResult 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 = sendRequestWithProfile(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationService.ApiDetectionResult(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 TranslationService.ApiDetectionResult(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationService.ModelAccessResult 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 = buildSystemPrompt(\"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(),\n RequestMode.DIRECT_FORMATTED);\n String probeUserPrompt = \"\"\"\n \n [TEXT lines=1]\n 0|Hello\n [/TEXT]\n \n \"\"\";\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.send(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream());\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 TranslationService.ModelAccessResult(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 (InterruptedException e) {\n Thread.currentThread().interrupt();\n return new TranslationService.ModelAccessResult(false, modelId, 0, \"Model verification interrupted\");\n } catch (IOException | RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationService.ModelAccessResult(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n for (String line : value.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (trimmed.matches(\"0\\\\s*[||].+\")) {\n return true;\n }\n }\n return false;\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, false));\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 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(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 requestBody.addProperty(\"max_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n if (!thinkingEnabled) {\n requestBody.addProperty(\"temperature\", 0.1);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\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 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 requestBody.addProperty(\"max_output_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n requestBody.addProperty(\"temperature\", 0.1);\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 requestBody.addProperty(\"max_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n requestBody.addProperty(\"temperature\", 0.1);\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 systemPrompt, String userPrompt, 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.1);\n generationConfig.addProperty(\"maxOutputTokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\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 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 @Override\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n @Override\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-26.1/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067433.6031406, "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"}, "c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068303.9360905, "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.3/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067435.2909386, "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"}}, "fabric/SimpleTranslate-Fabric-1.21.5/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067435.3128257, "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"}, "c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068306.0875208, "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-26.1.1/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067435.3926528, "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.5/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067435.4732835, "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"}}, "fabric/SimpleTranslate-Fabric-26.1.1/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067435.483951, "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"}}, "fabric/SimpleTranslate-Fabric-1.21.2/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067435.8875084, "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"}}, "fabric/SimpleTranslate-Fabric-26.2/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067436.021627, "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/config/ModConfig.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067436.1908011, "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"}}, "neoforge/SimpleTranslate-NeoForge-1.21.11/src/main/java/com/yourname/simpletranslate/translation/VisionOcrTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067436.4998174, "n": 25202, "h": "681af65eab396562", "c": "package com.yourname.simpletranslate.translation;\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.config.ModConfig;\nimport com.yourname.simpletranslate.util.ModelOutputSanitizer;\nimport com.yourname.simpletranslate.util.TranslationCacheKeys;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URLEncoder;\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.Base64;\nimport java.util.Locale;\nimport java.util.concurrent.CompletableFuture;\n\npublic final class VisionOcrTranslationService implements OcrTranslationService {\n private static final int MAX_OCR_OUTPUT_TOKENS = 2048;\n private static final String ONE_PIXEL_PNG_BASE64 =\n \"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/p9sAAAAASUVORK5CYII=\";\n private final HttpClient httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n private final Gson gson = new Gson();\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n @Override\n public CompletableFuture translateImage(byte[] pngBytes, String imageHash,\n String sourceLanguage, String targetLanguage) {\n if (pngBytes == null || pngBytes.length == 0) {\n return CompletableFuture.completedFuture(OcrResult.failure(\"No screenshot data\"));\n }\n ActiveProfile profile = activeProfile();\n if (!profile.ready()) {\n return CompletableFuture.completedFuture(OcrResult.failure(profile.errorMessage()));\n }\n String source = sourceLanguage == null || sourceLanguage.isBlank() ? \"auto\" : sourceLanguage;\n String target = targetLanguage == null || targetLanguage.isBlank() ? \"zh_cn\" : targetLanguage;\n String prompt = buildOcrPrompt(source, target);\n String base64 = Base64.getEncoder().encodeToString(pngBytes);\n String queueKey = \"ocr:\" + TranslationCacheKeys.hashSource(profile.queueIdentity()\n + \"\\u0001\" + prompt + \"\\u0001\" + (imageHash == null ? \"\" : imageHash)\n + \"\\u0001\" + base64.length());\n return TranslationRequestQueue.submit(\n queueKey,\n SURFACE,\n TranslationRequestQueue.Priority.INTERACTIVE,\n 2,\n () -> sendOcrRequestBlocking(profile, prompt, base64))\n .thenApply(raw -> parseOcrResult(raw));\n }\n\n @Override\n public CompletableFuture verifyAccess() {\n return translateImage(Base64.getDecoder().decode(ONE_PIXEL_PNG_BASE64),\n \"one-pixel-vision-probe\", \"auto\", ModConfig.TARGET_LANGUAGE.get());\n }\n\n @Override\n public boolean isReady() {\n return activeProfile().ready();\n }\n\n @Override\n public String describeActiveProfile() {\n ActiveProfile profile = activeProfile();\n if (!profile.ready()) {\n return profile.errorMessage();\n }\n return profile.format().getDisplayName() + \" / \" + profile.model();\n }\n\n public static String buildOcrPrompt(String sourceLanguage, String targetLanguage) {\n String target = targetLanguage == null || targetLanguage.isBlank() ? \"zh_cn\" : targetLanguage;\n String source = sourceLanguage == null || sourceLanguage.isBlank() ? \"auto\" : sourceLanguage;\n return \"\"\"\n You are an OCR and translation engine for Minecraft screenshots.\n Read only visible text inside the image, in natural reading order.\n Ignore Minecraft world objects, cursor/crosshair, panels, borders, and non-text graphics.\n Source language: %s. Target language: %s.\n Return strict JSON only, with exactly:\n {\"sourceText\":\"...\",\"translationText\":\"...\"}\n If the image contains no readable text, return {\"sourceText\":\"\",\"translationText\":\"\"}.\n Do not add markdown, comments, explanations, or extra fields.\n \"\"\".formatted(source, target);\n }\n\n public static JsonObject buildRequestBodyForTest(ModConfig.ApiFormat format, String model,\n String prompt, String base64Png) {\n EndpointKind kind = endpointKind(format);\n return buildRequestBody(kind, model == null || model.isBlank() ? format.getDefaultModel() : model,\n prompt == null ? \"\" : prompt, base64Png == null ? \"\" : base64Png);\n }\n\n public static OcrResult parseOcrResultForTest(String raw) {\n return parseOcrResult(raw);\n }\n\n private String sendOcrRequestBlocking(ActiveProfile profile, String prompt, String base64Png) {\n try {\n JsonObject body = buildRequestBody(profile.kind(), profile.model(), prompt, base64Png);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile)))\n .timeout(Duration.ofSeconds(60))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(body), StandardCharsets.UTF_8));\n applyAuthHeader(builder, profile);\n HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));\n if (response.statusCode() == 200) {\n return extractAssistantContent(response.body());\n }\n String message = compactError(response.body());\n if (response.statusCode() == 429 || response.statusCode() >= 500) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (message.isBlank() ? \"\" : \": \" + message));\n }\n SimpleTranslateMod.getLogger().warn(\"OCR vision API error status={} message={}\",\n response.statusCode(), message);\n return \"\";\n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n return \"\";\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"OCR I/O error\" : e.getMessage(), e);\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n throw e;\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().warn(\"OCR vision request failed: {}\",\n e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());\n return \"\";\n }\n }\n\n private static JsonObject buildRequestBody(EndpointKind kind, String model, String prompt, String base64Png) {\n return switch (kind) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(model, prompt, base64Png);\n case RESPONSES -> buildResponsesBody(model, prompt, base64Png);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, prompt, base64Png);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(prompt, base64Png);\n };\n }\n\n private static JsonObject buildChatCompletionsBody(String model, String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n root.addProperty(\"model\", model);\n root.addProperty(\"temperature\", 0);\n root.addProperty(\"max_tokens\", MAX_OCR_OUTPUT_TOKENS);\n\n JsonArray messages = new JsonArray();\n JsonObject user = new JsonObject();\n user.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject text = new JsonObject();\n text.addProperty(\"type\", \"text\");\n text.addProperty(\"text\", prompt);\n content.add(text);\n JsonObject image = new JsonObject();\n image.addProperty(\"type\", \"image_url\");\n JsonObject imageUrl = new JsonObject();\n imageUrl.addProperty(\"url\", dataUrl(base64Png));\n imageUrl.addProperty(\"detail\", \"high\");\n image.add(\"image_url\", imageUrl);\n content.add(image);\n user.add(\"content\", content);\n messages.add(user);\n root.add(\"messages\", messages);\n return root;\n }\n\n private static JsonObject buildResponsesBody(String model, String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n root.addProperty(\"model\", model);\n root.addProperty(\"max_output_tokens\", MAX_OCR_OUTPUT_TOKENS);\n\n JsonArray input = new JsonArray();\n JsonObject user = new JsonObject();\n user.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject text = new JsonObject();\n text.addProperty(\"type\", \"input_text\");\n text.addProperty(\"text\", prompt);\n content.add(text);\n JsonObject image = new JsonObject();\n image.addProperty(\"type\", \"input_image\");\n image.addProperty(\"image_url\", dataUrl(base64Png));\n image.addProperty(\"detail\", \"high\");\n content.add(image);\n user.add(\"content\", content);\n input.add(user);\n root.add(\"input\", input);\n return root;\n }\n\n private static JsonObject buildAnthropicMessagesBody(String model, String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n root.addProperty(\"model\", model);\n root.addProperty(\"max_tokens\", MAX_OCR_OUTPUT_TOKENS);\n root.addProperty(\"temperature\", 0);\n root.addProperty(\"system\", \"Return strict JSON only.\");\n\n JsonArray messages = new JsonArray();\n JsonObject user = new JsonObject();\n user.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject image = new JsonObject();\n image.addProperty(\"type\", \"image\");\n JsonObject source = new JsonObject();\n source.addProperty(\"type\", \"base64\");\n source.addProperty(\"media_type\", \"image/png\");\n source.addProperty(\"data\", base64Png);\n image.add(\"source\", source);\n content.add(image);\n JsonObject text = new JsonObject();\n text.addProperty(\"type\", \"text\");\n text.addProperty(\"text\", prompt);\n content.add(text);\n user.add(\"content\", content);\n messages.add(user);\n root.add(\"messages\", messages);\n return root;\n }\n\n private static JsonObject buildGeminiGenerateContentBody(String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n JsonArray parts = new JsonArray();\n JsonObject image = new JsonObject();\n JsonObject inlineData = new JsonObject();\n inlineData.addProperty(\"mimeType\", \"image/png\");\n inlineData.addProperty(\"data\", base64Png);\n image.add(\"inlineData\", inlineData);\n parts.add(image);\n JsonObject text = new JsonObject();\n text.addProperty(\"text\", prompt);\n parts.add(text);\n content.add(\"parts\", parts);\n contents.add(content);\n root.add(\"contents\", contents);\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0);\n generationConfig.addProperty(\"maxOutputTokens\", MAX_OCR_OUTPUT_TOKENS);\n root.add(\"generationConfig\", generationConfig);\n return root;\n }\n\n private ActiveProfile activeProfile() {\n boolean reuse = ModConfig.OCR_USE_TRANSLATION_MODEL.get();\n ModConfig.ApiFormat format = reuse ? ModConfig.API_FORMAT.get() : ModConfig.OCR_API_FORMAT.get();\n if (!isVisionFormat(format)) {\n return ActiveProfile.error(format, \"OCR model format does not support image input\");\n }\n String key = sanitizeApiKey(reuse ? ModConfig.DEEPSEEK_API_KEY.get() : ModConfig.OCR_API_KEY.get());\n if (key.isBlank()) {\n return ActiveProfile.error(format, \"OCR API key not configured\");\n }\n String url = ModConfig.normalizeApiUrl(reuse ? ModConfig.DEEPSEEK_API_URL.get() : ModConfig.OCR_API_URL.get());\n String model = reuse\n ? normalizedTranslationModel(format, ModConfig.DEEPSEEK_MODEL.get())\n : normalizedOcrModel(format, ModConfig.OCR_MODEL.get());\n if (model.isBlank()) {\n return ActiveProfile.error(format, \"OCR model not configured\");\n }\n EndpointKind kind = endpointKind(format);\n String endpoint = resolveEndpoint(url, kind, model);\n return new ActiveProfile(true, \"\", format, kind, url, endpoint, key, model);\n }\n\n private static boolean isVisionFormat(ModConfig.ApiFormat format) {\n return format == ModConfig.ApiFormat.OPENAI_RESPONSES\n || format == ModConfig.ApiFormat.OPENAI_CHAT_COMPAT\n || format == ModConfig.ApiFormat.ANTHROPIC_MESSAGES\n || format == ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT;\n }\n\n private static EndpointKind endpointKind(ModConfig.ApiFormat format) {\n if (format == ModConfig.ApiFormat.OPENAI_RESPONSES) {\n return EndpointKind.RESPONSES;\n }\n if (format == ModConfig.ApiFormat.ANTHROPIC_MESSAGES) {\n return EndpointKind.ANTHROPIC_MESSAGES;\n }\n if (format == ModConfig.ApiFormat.GEMINI_GENERATE_CONTENT) {\n return EndpointKind.GEMINI_GENERATE_CONTENT;\n }\n return EndpointKind.CHAT_COMPLETIONS;\n }\n\n private static String normalizedTranslationModel(ModConfig.ApiFormat format, String model) {\n if (format == ModConfig.ApiFormat.DEEPSEEK_CHAT) {\n return \"\";\n }\n return model == null || model.isBlank() ? format.getDefaultModel() : model.trim();\n }\n\n private static String normalizedOcrModel(ModConfig.ApiFormat format, String model) {\n return model == null || model.isBlank() ? format.getDefaultModel() : model.trim();\n }\n\n private static String resolveEndpoint(String apiUrl, EndpointKind kind, String model) {\n String normalized = ModConfig.normalizeApiUrl(apiUrl);\n return switch (kind) {\n case CHAT_COMPLETIONS -> resolveChatCompletionsEndpoint(normalized);\n case RESPONSES -> resolveResponsesEndpoint(normalized);\n case ANTHROPIC_MESSAGES -> resolveAnthropicMessagesEndpoint(normalized);\n case GEMINI_GENERATE_CONTENT -> resolveGeminiGenerateContentEndpoint(normalized, model);\n };\n }\n\n private static String resolveChatCompletionsEndpoint(String normalized) {\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private static String resolveResponsesEndpoint(String normalized) {\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 static String resolveAnthropicMessagesEndpoint(String normalized) {\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 static String resolveGeminiGenerateContentEndpoint(String normalized, String model) {\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 void applyAuthHeader(HttpRequest.Builder builder, ActiveProfile profile) {\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", profile.apiKey());\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + profile.apiKey());\n }\n }\n\n private String withApiKeyQuery(ActiveProfile profile) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(profile.apiKey(), StandardCharsets.UTF_8);\n }\n\n private static String extractAssistantContent(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return \"\";\n }\n try {\n JsonObject response = JsonParser.parseString(responseBody).getAsJsonObject();\n String extracted = extractAssistantContent(response);\n return extracted == null ? \"\" : extracted;\n } catch (Exception ignored) {\n return responseBody;\n }\n }\n\n private static String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return \"\";\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 String content = textFromJsonContent(message.get(\"content\"));\n if (!content.isBlank()) {\n return content;\n }\n }\n }\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 String text = textFromOutputArray(response.getAsJsonArray(\"output\"));\n if (!text.isBlank()) {\n return text;\n }\n }\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n String text = textFromJsonContent(response.get(\"content\"));\n if (!text.isBlank()) {\n return text;\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\")) {\n return textFromJsonContent(content.get(\"parts\"));\n }\n }\n }\n }\n return \"\";\n }\n\n private static String textFromOutputArray(JsonArray output) {\n StringBuilder builder = new StringBuilder();\n for (JsonElement element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject object = element.getAsJsonObject();\n if (object.has(\"content\")) {\n String content = textFromJsonContent(object.get(\"content\"));\n if (!content.isBlank()) {\n if (!builder.isEmpty()) {\n builder.append('\\n');\n }\n builder.append(content);\n }\n }\n }\n return builder.toString();\n }\n\n private static String textFromJsonContent(JsonElement element) {\n if (element == null || element.isJsonNull()) {\n return \"\";\n }\n if (element.isJsonPrimitive()) {\n return element.getAsString();\n }\n if (!element.isJsonArray()) {\n return \"\";\n }\n StringBuilder builder = new StringBuilder();\n for (JsonElement item : element.getAsJsonArray()) {\n if (!item.isJsonObject()) {\n continue;\n }\n JsonObject part = item.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n if (!builder.isEmpty()) {\n builder.append('\\n');\n }\n builder.append(part.get(\"text\").getAsString());\n }\n }\n return builder.toString();\n }\n\n private static OcrResult parseOcrResult(String raw) {\n String cleaned = stripFences(ModelOutputSanitizer.sanitize(raw));\n if (cleaned == null || cleaned.isBlank()) {\n return OcrResult.success(\"\", \"\");\n }\n String json = objectSlice(cleaned);\n if (!json.isBlank()) {\n try {\n JsonObject object = JsonParser.parseString(json).getAsJsonObject();\n String source = stringField(object, \"sourceText\");\n String translation = stringField(object, \"translationText\");\n return OcrResult.success(source, translation);\n } catch (Exception ignored) {\n // Fall through to non-JSON recovery.\n }\n }\n return OcrResult.success(\"\", cleaned);\n }\n\n private static String stripFences(String value) {\n if (value == null) {\n return \"\";\n }\n String text = value.trim();\n if (text.startsWith(\"```\")) {\n text = text.replaceFirst(\"(?is)^```(?:json|text)?\\\\s*\", \"\");\n text = text.replaceFirst(\"(?is)\\\\s*```$\", \"\");\n }\n return text.trim();\n }\n\n private static String objectSlice(String value) {\n int start = value == null ? -1 : value.indexOf('{');\n int end = value == null ? -1 : value.lastIndexOf('}');\n if (start < 0 || end <= start) {\n return \"\";\n }\n return value.substring(start, end + 1);\n }\n\n private static String stringField(JsonObject object, String key) {\n if (object == null || !object.has(key) || object.get(key).isJsonNull()) {\n return \"\";\n }\n try {\n return object.get(key).getAsString();\n } catch (Exception ignored) {\n return \"\";\n }\n }\n\n private static String sanitizeApiKey(String apiKey) {\n if (apiKey == null) {\n return \"\";\n }\n return apiKey.trim();\n }\n\n private static String dataUrl(String base64Png) {\n return \"data:image/png;base64,\" + (base64Png == null ? \"\" : base64Png);\n }\n\n private static String compactError(String body) {\n if (body == null || body.isBlank()) {\n return \"\";\n }\n String compact = body.replace('\\n', ' ').replace('\\r', ' ').trim();\n return compact.length() > 240 ? compact.substring(0, 237) + \"...\" : compact;\n }\n\n private record ActiveProfile(\n boolean ready,\n String errorMessage,\n ModConfig.ApiFormat format,\n EndpointKind kind,\n String endpointBase,\n String endpointUrl,\n String apiKey,\n String model) {\n static ActiveProfile error(ModConfig.ApiFormat format, String message) {\n return new ActiveProfile(false, message == null ? \"OCR not configured\" : message,\n format == null ? ModConfig.ApiFormat.OPENAI_RESPONSES : format,\n EndpointKind.RESPONSES, \"\", \"\", \"\", \"\");\n }\n\n String queueIdentity() {\n return (format == null ? \"\" : format.name().toLowerCase(Locale.ROOT))\n + \"|\" + endpointBase + \"|\" + endpointUrl + \"|\" + model;\n }\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.20.3/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067437.469297, "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"}}, "fabric/SimpleTranslate-Fabric-1.19.2/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067437.6760628, "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"}, "c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068303.7627568, "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-26.1.2/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067438.0341518, "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"}}, "neoforge/SimpleTranslate-NeoForge-1.21.8/src/main/java/com/yourname/simpletranslate/translation/DeepSeekTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067438.0451193, "n": 60896, "h": "4db9c81e047177fe", "c": "package com.yourname.simpletranslate.translation;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.util.ModelOutputSanitizer;\nimport com.yourname.simpletranslate.util.TranslationCacheKeys;\nimport com.yourname.simpletranslate.util.TranslationMapping;\nimport com.yourname.simpletranslate.util.TranslationMappingResult;\nimport com.yourname.simpletranslate.util.TranslationSlot;\nimport com.yourname.simpletranslate.util.TranslationTextDetector;\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.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 private static final Pattern ALLOWED_MAPPING_PLACEHOLDER_PATTERN = Pattern.compile(\"@@@F\\\\d+@@@\");\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 = 8192;\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 DIRECT_SURFACE_PATTERN = Pattern.compile(\"]*\\\\bsurface=\\\"([^\\\"]+)\\\"\");\n private static final Pattern MAPPING_SURFACE_PATTERN = Pattern.compile(\"\\\"surface\\\"\\\\s*:\\\\s*\\\"([^\\\"]+)\\\"\");\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 /** Alias so existing references keep working after the prompt-builder split. */\n private static final class RequestMode {\n static final TranslationPrompts.RequestMode PLAIN_TEXT = TranslationPrompts.RequestMode.PLAIN_TEXT;\n static final TranslationPrompts.RequestMode PRESERVE_MARKERS = TranslationPrompts.RequestMode.PRESERVE_MARKERS;\n static final TranslationPrompts.RequestMode MAPPING_JSON = TranslationPrompts.RequestMode.MAPPING_JSON;\n static final TranslationPrompts.RequestMode DIRECT_FORMATTED = TranslationPrompts.RequestMode.DIRECT_FORMATTED;\n\n private RequestMode() {\n }\n }\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 @Override\n public CompletableFuture translate(String text, String sourceLanguage, String targetLanguage) {\n return translateWithTerms(text, sourceLanguage, targetLanguage, List.of());\n }\n\n @Override\n public CompletableFuture translateWithTerms(String text, String sourceLanguage, String targetLanguage,\n List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.PLAIN_TEXT);\n return sendRequest(systemPrompt, text);\n }\n\n @Override\n public CompletableFuture translatePreservingMarkers(String text, String sourceLanguage,\n String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.PRESERVE_MARKERS);\n return sendRequest(systemPrompt, text);\n }\n\n @Override\n public CompletableFuture translateFormattedDocument(String document, String surface,\n String sourceLanguage, String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n // Style/term hints live in the user payload so the system prompt stays\n // byte-stable per language pair (provider prefix caching).\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, List.of(),\n RequestMode.DIRECT_FORMATTED);\n String userPrompt = insertDirectSections(document, TranslationPrompts.directUserSections(termHints));\n return sendRequest(systemPrompt, userPrompt, estimateDirectMaxTokens(document), surface);\n }\n\n /** Same as {@link #translateFormattedDocument} but scales the output token budget. */\n public CompletableFuture translateFormattedDocument(String document, String surface,\n String sourceLanguage, String targetLanguage, List termHints, int maxTokenMultiplier) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, List.of(),\n RequestMode.DIRECT_FORMATTED);\n String userPrompt = insertDirectSections(document, TranslationPrompts.directUserSections(termHints));\n return sendRequest(systemPrompt, userPrompt, estimateDirectMaxTokens(document, maxTokenMultiplier), surface);\n }\n\n /** Inserts [STYLE]/[TERMS] sections right before the [TEXT] block. */\n private static String insertDirectSections(String document, String sections) {\n if (sections == null || sections.isBlank() || document == null) {\n return document;\n }\n int index = document.indexOf(\"[TEXT\");\n if (index < 0) {\n return sections + document;\n }\n return document.substring(0, index) + sections + document.substring(index);\n }\n\n /** Output budget follows the [TEXT] block size, not the whole payload with context. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Output budget follows the [TEXT] block; multiplier supports partial retries. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int start = body.indexOf(\"[TEXT\");\n int end = body.lastIndexOf(\"[/TEXT]\");\n if (start >= 0 && end > start) {\n body = body.substring(start, end);\n }\n int lineCount = 0;\n for (String line : body.split(\"\\\\R\")) {\n if (line != null && line.matches(\"(?s).*?\\\\d{1,4}\\\\s*[||].*\")) {\n lineCount++;\n }\n }\n // Chinese game text often expands; multi-line tooltips need headroom per line.\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int lineEstimate = lineCount * 192 + 384;\n int estimated = Math.max(charEstimate, lineEstimate);\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 @Override\n public CompletableFuture translateMapping(TranslationMapping mapping,\n String sourceLanguage, String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.MAPPING_JSON);\n String userPrompt = buildMappingUserPrompt(mapping, sourceLanguage, targetLanguage);\n return sendRequest(systemPrompt, userPrompt).thenApply(raw -> parseMappingResponse(raw, mapping));\n }\n\n @Override\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationService.ApiDetectionResult(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 @Override\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationService.ModelDetectionResult(\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 @Override\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 TranslationService.ModelAccessResult(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationService.ModelAccessResult(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationService.ModelAccessResult(\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 String buildSystemPrompt(String sourceLanguage, String targetLanguage, List termHints,\n TranslationPrompts.RequestMode mode) {\n return TranslationPrompts.buildSystemPrompt(sourceLanguage, targetLanguage, termHints, mode);\n }\n\n\n private String buildMappingUserPrompt(TranslationMapping mapping, String sourceLanguage, String targetLanguage) {\n JsonObject root = new JsonObject();\n root.addProperty(\"version\", \"mapping-v1\");\n root.addProperty(\"jobId\", mapping.taskId());\n root.addProperty(\"surface\", mapping.surface());\n root.addProperty(\"sourceLanguage\", getLanguageName(sourceLanguage));\n root.addProperty(\"targetLanguage\", getLanguageName(targetLanguage));\n root.addProperty(\"context\", mapping.context());\n root.addProperty(\"layoutSignature\", mapping.layoutSignature());\n root.addProperty(\"styleSignature\", mapping.styleSignature());\n boolean flexibleLayout = isFlexibleMappingSurface(mapping.surface());\n root.addProperty(\"layoutMode\", flexibleLayout ? \"flexible-tooltip\" : \"fixed-layout\");\n root.addProperty(\"instructions\", flexibleLayout\n ? \"Translate every unit as its own semantic style block or styled token. Use natural Minecraft localization for the target language, ignore source visual line breaks, keep every @@@F marker exactly once in the same unit, and preserve only structural tokens such as numbers, ids, commands, control codes, and placeholders.\"\n : \"Translate each fixed-layout slot conservatively. Preserve line/slot structure, technical tokens, and every @@@F marker exactly.\");\n\n JsonArray units = new JsonArray();\n for (TranslationSlot slot : mapping.slots()) {\n JsonObject unit = new JsonObject();\n unit.addProperty(\"id\", slot.id());\n unit.addProperty(\"text\", slot.text());\n unit.addProperty(\"role\", slot.role());\n unit.addProperty(\"maxChars\", slot.maxWidth());\n unit.addProperty(\"fixed\", slot.fixedSlot());\n unit.addProperty(\"lineIndex\", slot.lineIndex());\n unit.addProperty(\"slotIndex\", slot.slotIndex());\n unit.addProperty(\"styleSignature\", slot.styleSignature());\n unit.addProperty(\"tokenMask\", slot.tokenMask());\n units.add(unit);\n }\n root.add(\"units\", units);\n return root.toString();\n }\n\n private boolean isFlexibleMappingSurface(String surface) {\n if (surface == null) {\n return false;\n }\n String normalized = surface.toLowerCase();\n return normalized.startsWith(\"tooltip.\")\n || normalized.startsWith(\"hover.\")\n || normalized.startsWith(\"book.\");\n }\n\n private String getLanguageName(String code) {\n return TranslationTextDetector.displayLanguageName(code);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt) {\n return sendRequest(systemPrompt, userPrompt, estimateMaxTokens(userPrompt));\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens) {\n return sendRequest(systemPrompt, userPrompt, maxTokens, null);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String explicitSurface) {\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 surface = explicitSurface == null || explicitSurface.isBlank()\n ? detectRequestSurface(userPrompt)\n : explicitSurface;\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestBlocking(profiles, apiKey, model, systemPrompt, userPrompt, maxTokens));\n }\n\n private String sendRequestBlocking(List profiles, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n for (ApiRequestProfile profile : orderedProfiles) {\n String translated = sendRequestWithProfile(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return translated;\n }\n }\n return null;\n }\n\n private String sendRequestWithProfile(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return null;\n }\n\n long startedAt = System.nanoTime();\n try {\n HttpRequest request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n String result = parseResponse(response.body(), startedAt);\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 return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\"Empty translation result from DeepSeek\");\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 (InterruptedException e) {\n Thread.currentThread().interrupt();\n SimpleTranslateMod.getLogger().error(\"Translation request interrupted\", e);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n throw e;\n } catch (InvalidApiResponseException e) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\", e.getMessage());\n return null;\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\", e.getMessage(), e);\n return null;\n }\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 String detectRequestSurface(String userPrompt) {\n if (userPrompt == null || userPrompt.isBlank()) {\n return \"manager.raw\";\n }\n Matcher direct = DIRECT_SURFACE_PATTERN.matcher(userPrompt);\n if (direct.find()) {\n return direct.group(1);\n }\n Matcher mapping = MAPPING_SURFACE_PATTERN.matcher(userPrompt);\n if (mapping.find()) {\n return mapping.group(1);\n }\n return \"manager.raw\";\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 static int estimateMaxTokens(String text) {\n int length = text == null ? 0 : text.length();\n int estimated = length + 512;\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n return MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n return Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.contains(\"data:\")) {\n return parseStreamingResponse(responseBody);\n }\n return parsePlainResponse(responseBody);\n }\n\n private String parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return null;\n }\n\n Future readerTask = 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 null;\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 String parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return null;\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\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 null;\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);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return null;\n }\n\n if (sawData) {\n return ModelOutputSanitizer.sanitize(result.toString());\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 String parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\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);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return null;\n }\n return ModelOutputSanitizer.sanitize(result.toString());\n }\n\n private String parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\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 ModelOutputSanitizer.sanitize(raw);\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 TranslationMappingResult parseMappingResponse(String response, TranslationMapping mapping) {\n String sanitized = ModelOutputSanitizer.sanitize(response);\n if (sanitized == null || sanitized.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n try {\n String jsonPayload = extractJsonObjectPayload(sanitized);\n if (jsonPayload == null || jsonPayload.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n JsonObject root = JsonParser.parseString(jsonPayload).getAsJsonObject();\n String version = root.has(\"version\") ? root.get(\"version\").getAsString() : \"\";\n String jobId = root.has(\"jobId\") ? root.get(\"jobId\").getAsString() : \"\";\n if (!\"mapping-v1\".equals(version) || !mapping.taskId().equals(jobId)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n if (!root.has(\"translations\") || !root.get(\"translations\").isJsonArray()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n Map translations = new LinkedHashMap<>();\n for (var element : root.getAsJsonArray(\"translations\")) {\n if (!element.isJsonObject()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n JsonObject item = element.getAsJsonObject();\n String id = item.has(\"id\") ? item.get(\"id\").getAsString() : \"\";\n String translation = item.has(\"translation\") ? item.get(\"translation\").getAsString() : \"\";\n if (id.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n String cleaned = ModelOutputSanitizer.sanitize(translation);\n if (cleaned == null || cleaned.isBlank() || containsDisallowedMappingTokens(cleaned)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n translations.put(id, cleaned);\n }\n\n if (translations.size() != mapping.slots().size()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n for (TranslationSlot slot : mapping.slots()) {\n if (!translations.containsKey(slot.id())) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n if (slot.fixedSlot()) {\n String normalizedSource = TranslationCacheKeys.normalizeSource(slot.text());\n String normalizedTranslated = TranslationCacheKeys.normalizeSource(translations.get(slot.id()));\n if (!normalizedSource.equals(normalizedTranslated)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n }\n }\n\n return new TranslationMappingResult(mapping.taskId(), translations);\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().debug(\"Failed to parse mapping response\", e);\n return new TranslationMappingResult(mapping.taskId(), Map.of());\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 boolean containsDisallowedMappingTokens(String text) {\n if (text == null || text.isEmpty()) {\n return false;\n }\n return containsDisallowedMappingMarker(text)\n || text.contains(\"```\")\n || text.contains(\"\")\n || text.contains(\"\")\n || text.contains(\"\")\n || text.contains(\"\");\n }\n\n private boolean containsDisallowedMappingMarker(String text) {\n int index = text.indexOf(\"@@@\");\n if (index < 0) {\n return false;\n }\n Matcher matcher = ALLOWED_MAPPING_PLACEHOLDER_PATTERN.matcher(text);\n int cursor = 0;\n while (matcher.find()) {\n String between = text.substring(cursor, matcher.start());\n if (between.contains(\"@@@\")) {\n return true;\n }\n cursor = matcher.end();\n }\n return text.substring(cursor).contains(\"@@@\");\n }\n\n private void appendStreamingChunk(StringBuilder result, String json) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\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 TranslationService.ApiDetectionResult 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 = sendRequestWithProfile(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationService.ApiDetectionResult(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 TranslationService.ApiDetectionResult(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationService.ModelAccessResult 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 = buildSystemPrompt(\"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(),\n RequestMode.DIRECT_FORMATTED);\n String probeUserPrompt = \"\"\"\n \n [TEXT lines=1]\n 0|Hello\n [/TEXT]\n \n \"\"\";\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.send(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream());\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 TranslationService.ModelAccessResult(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 (InterruptedException e) {\n Thread.currentThread().interrupt();\n return new TranslationService.ModelAccessResult(false, modelId, 0, \"Model verification interrupted\");\n } catch (IOException | RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationService.ModelAccessResult(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n for (String line : value.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (trimmed.matches(\"0\\\\s*[||].+\")) {\n return true;\n }\n }\n return false;\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, false));\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 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(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 requestBody.addProperty(\"max_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n if (!thinkingEnabled) {\n requestBody.addProperty(\"temperature\", 0.1);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\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 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 requestBody.addProperty(\"max_output_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n requestBody.addProperty(\"temperature\", 0.1);\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 requestBody.addProperty(\"max_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n requestBody.addProperty(\"temperature\", 0.1);\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 systemPrompt, String userPrompt, 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.1);\n generationConfig.addProperty(\"maxOutputTokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\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 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 @Override\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n @Override\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.1/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067438.0459611, "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.3/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067438.1244307, "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/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067438.1356966, "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.4/src/main/java/com/yourname/simpletranslate/translation/VisionOcrTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067438.1959064, "n": 34258, "h": "6b065070055b19c4", "c": "package com.yourname.simpletranslate.translation;\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.config.ModConfig;\nimport com.yourname.simpletranslate.util.ModelOutputSanitizer;\nimport com.yourname.simpletranslate.util.TranslationCacheKeys;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URLEncoder;\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.Base64;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.concurrent.CompletableFuture;\n\npublic final class VisionOcrTranslationService implements OcrTranslationService {\n private static final int MAX_OCR_OUTPUT_TOKENS = 2048;\n private static final String OCR_ERROR_PREFIX = \"\\u0000simpletranslate-ocr-error:\";\n private static final String OCR_PROBE_PNG_BASE64 =\n \"iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFnSURBVGhD7ZbRccMgEEQpj4Ioh17Uijohg+XYxwLHnp1kJj7eDD/Ia+BxIIXinIAd3tgCsMMbWwB2eGMLwA5vbAHY4Q2TgDPHEkKAFks+8Zc9w2zMZRQ9Eo7Rt8gMSkAKOEoaTIJZDJNNByQIAbeGwRcgBKwXMJ8Qm01FJmkBZPVpLAW0pTsY8EjNpKSDNtsuEuXIkn4KwMyF/N93j8JCwFlyVBb/jZTwMCCz44WUM5eoHgE99wcCxC7hLBvEYh93AZvtWQmQFWD86w5dgMF0N2lDFuHvgLEgC/9awLu7X6EF6GU8OAJ0tkcXoNxFL6AL+O1L8H5PYIV01VSRQn9o9ysLAfgq6wfWnlteg7JKhgIqjQRlQwwsBXQT1RraobPtYqYCrofP3PTrk4cQUCEWMp3MOoveVAFwR+DxsUIKuANffVfjShGPyq1NpK0EoFQUaMEm4APZArDDG1sAdnhjC8AOb7gX8AW1uKq6OGly1gAAAABJRU5ErkJggg==\";\n private final HttpClient httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n private final Gson gson = new Gson();\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n @Override\n public CompletableFuture translateImage(byte[] pngBytes, String imageHash,\n String sourceLanguage, String targetLanguage) {\n if (pngBytes == null || pngBytes.length == 0) {\n return CompletableFuture.completedFuture(OcrResult.failure(\"No screenshot data\"));\n }\n ActiveProfile profile = activeProfile();\n if (!profile.ready()) {\n return CompletableFuture.completedFuture(OcrResult.failure(profile.errorMessage()));\n }\n String source = sourceLanguage == null || sourceLanguage.isBlank() ? \"auto\" : sourceLanguage;\n String target = targetLanguage == null || targetLanguage.isBlank() ? \"zh_cn\" : targetLanguage;\n String prompt = buildOcrPrompt(source, target);\n String base64 = Base64.getEncoder().encodeToString(pngBytes);\n String queueKey = \"ocr:\" + TranslationCacheKeys.hashSource(profile.queueIdentity()\n + \"\\u0001\" + prompt + \"\\u0001\" + (imageHash == null ? \"\" : imageHash)\n + \"\\u0001\" + base64.length());\n return TranslationRequestQueue.submit(\n queueKey,\n SURFACE,\n TranslationRequestQueue.Priority.INTERACTIVE,\n 2,\n () -> sendOcrRequestBlocking(profile, prompt, base64))\n .thenApply(raw -> raw == null\n ? OcrResult.failure(\"OCR request failed after retries\")\n : parseOcrResult(raw));\n }\n\n @Override\n public CompletableFuture verifyAccess() {\n return translateImage(Base64.getDecoder().decode(OCR_PROBE_PNG_BASE64),\n \"ocr-vision-probe\", \"auto\", ModConfig.TARGET_LANGUAGE.get());\n }\n\n public CompletableFuture verifyAccess(boolean reuseTranslationModel,\n ModConfig.ApiFormat format,\n String apiUrl,\n String apiKey,\n String model) {\n ActiveProfile profile = reuseTranslationModel\n ? activeProfileFromConfig(true)\n : activeProfileFromValues(format, apiUrl, apiKey, model);\n if (!profile.ready()) {\n return CompletableFuture.completedFuture(OcrResult.failure(profile.errorMessage()));\n }\n String prompt = buildOcrPrompt(\"auto\", ModConfig.TARGET_LANGUAGE.get());\n String base64 = OCR_PROBE_PNG_BASE64;\n String queueKey = \"ocr-probe:\" + TranslationCacheKeys.hashSource(profile.queueIdentity());\n return TranslationRequestQueue.submit(\n queueKey,\n SURFACE + \".probe\",\n TranslationRequestQueue.Priority.INTERACTIVE,\n 1,\n () -> sendOcrRequestBlocking(profile, prompt, base64))\n .thenApply(raw -> isValidOcrProbeResponse(raw)\n ? parseOcrResult(raw)\n : OcrResult.failure(\"OCR model did not return the required JSON fields\"));\n }\n\n @Override\n public boolean isReady() {\n return activeProfile().ready();\n }\n\n @Override\n public String describeActiveProfile() {\n ActiveProfile profile = activeProfile();\n if (!profile.ready()) {\n return profile.errorMessage();\n }\n return profile.format().getDisplayName() + \" / \" + profile.model();\n }\n\n public static String buildOcrPrompt(String sourceLanguage, String targetLanguage) {\n String target = targetLanguage == null || targetLanguage.isBlank() ? \"zh_cn\" : targetLanguage;\n String source = sourceLanguage == null || sourceLanguage.isBlank() ? \"auto\" : sourceLanguage;\n return \"\"\"\n You are an OCR and translation engine for Minecraft screenshots.\n Read only visible text inside the image, in natural reading order.\n Ignore Minecraft world objects, cursor/crosshair, panels, borders, and non-text graphics.\n Source language: %s. Target language: %s.\n Return strict JSON only, with exactly:\n {\"sourceText\":\"all source text\",\"translationText\":\"all translated text\",\"regions\":[{\"sourceText\":\"one source block\",\"translationText\":\"its translation\",\"x\":0,\"y\":0,\"width\":1000,\"height\":1000}]}\n regions must contain one entry per visible text line. Do not merge separate lines or distant labels.\n x, y, width, and height are integers normalized to 0..1000 relative to the image.\n Each region must be the tight bounding rectangle of the source glyph pixels, not a loose panel,\n paragraph, translated-text estimate, or surrounding UI container.\n Keep regions in natural reading order. Every translation must correspond only to the source text\n inside that exact region.\n If the image contains no readable text, return {\"sourceText\":\"\",\"translationText\":\"\",\"regions\":[]}.\n Do not add markdown, comments, explanations, or extra fields.\n \"\"\".formatted(source, target);\n }\n\n public static JsonObject buildRequestBodyForTest(ModConfig.ApiFormat format, String model,\n String prompt, String base64Png) {\n EndpointKind kind = endpointKind(format);\n return buildRequestBody(kind, model == null || model.isBlank() ? format.getDefaultModel() : model,\n prompt == null ? \"\" : prompt, base64Png == null ? \"\" : base64Png);\n }\n\n public static OcrResult parseOcrResultForTest(String raw) {\n return parseOcrResult(raw);\n }\n\n private String sendOcrRequestBlocking(ActiveProfile profile, String prompt, String base64Png) {\n try {\n JsonObject body = buildRequestBody(profile.kind(), profile.model(), prompt, base64Png);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile)))\n .timeout(Duration.ofSeconds(60))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(body), StandardCharsets.UTF_8));\n applyAuthHeader(builder, profile);\n HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));\n if (response.statusCode() == 200) {\n if (looksLikeHtml(response.body())) {\n return failurePayload(\"OCR endpoint returned an HTML page; check API URL and interface format\");\n }\n String content = extractAssistantContent(response.body());\n return content == null || content.isBlank()\n ? failurePayload(\"OCR API returned an empty response\")\n : content;\n }\n String message = compactError(response.body());\n if (response.statusCode() == 429 || response.statusCode() >= 500) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (message.isBlank() ? \"\" : \": \" + message));\n }\n SimpleTranslateMod.getLogger().warn(\"OCR vision API error status={} message={}\",\n response.statusCode(), message);\n return failurePayload(\"HTTP \" + response.statusCode() + (message.isBlank() ? \"\" : \": \" + message));\n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n return failurePayload(\"OCR request interrupted\");\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"OCR I/O error\" : e.getMessage(), e);\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n throw e;\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().warn(\"OCR vision request failed: {}\",\n e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());\n return failurePayload(e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());\n }\n }\n\n private static JsonObject buildRequestBody(EndpointKind kind, String model, String prompt, String base64Png) {\n return switch (kind) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(model, prompt, base64Png);\n case RESPONSES -> buildResponsesBody(model, prompt, base64Png);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, prompt, base64Png);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(prompt, base64Png);\n };\n }\n\n private static JsonObject buildChatCompletionsBody(String model, String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n root.addProperty(\"model\", model);\n root.addProperty(\"temperature\", 0);\n root.addProperty(\"max_tokens\", MAX_OCR_OUTPUT_TOKENS);\n\n JsonArray messages = new JsonArray();\n JsonObject user = new JsonObject();\n user.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject text = new JsonObject();\n text.addProperty(\"type\", \"text\");\n text.addProperty(\"text\", prompt);\n content.add(text);\n JsonObject image = new JsonObject();\n image.addProperty(\"type\", \"image_url\");\n JsonObject imageUrl = new JsonObject();\n imageUrl.addProperty(\"url\", dataUrl(base64Png));\n imageUrl.addProperty(\"detail\", \"high\");\n image.add(\"image_url\", imageUrl);\n content.add(image);\n user.add(\"content\", content);\n messages.add(user);\n root.add(\"messages\", messages);\n return root;\n }\n\n private static JsonObject buildResponsesBody(String model, String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n root.addProperty(\"model\", model);\n root.addProperty(\"max_output_tokens\", MAX_OCR_OUTPUT_TOKENS);\n\n JsonArray input = new JsonArray();\n JsonObject user = new JsonObject();\n user.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject text = new JsonObject();\n text.addProperty(\"type\", \"input_text\");\n text.addProperty(\"text\", prompt);\n content.add(text);\n JsonObject image = new JsonObject();\n image.addProperty(\"type\", \"input_image\");\n image.addProperty(\"image_url\", dataUrl(base64Png));\n image.addProperty(\"detail\", \"high\");\n content.add(image);\n user.add(\"content\", content);\n input.add(user);\n root.add(\"input\", input);\n return root;\n }\n\n private static JsonObject buildAnthropicMessagesBody(String model, String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n root.addProperty(\"model\", model);\n root.addProperty(\"max_tokens\", MAX_OCR_OUTPUT_TOKENS);\n root.addProperty(\"temperature\", 0);\n root.addProperty(\"system\", \"Return strict JSON only.\");\n\n JsonArray messages = new JsonArray();\n JsonObject user = new JsonObject();\n user.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject image = new JsonObject();\n image.addProperty(\"type\", \"image\");\n JsonObject source = new JsonObject();\n source.addProperty(\"type\", \"base64\");\n source.addProperty(\"media_type\", \"image/png\");\n source.addProperty(\"data\", base64Png);\n image.add(\"source\", source);\n content.add(image);\n JsonObject text = new JsonObject();\n text.addProperty(\"type\", \"text\");\n text.addProperty(\"text\", prompt);\n content.add(text);\n user.add(\"content\", content);\n messages.add(user);\n root.add(\"messages\", messages);\n return root;\n }\n\n private static JsonObject buildGeminiGenerateContentBody(String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n JsonArray parts = new JsonArray();\n JsonObject image = new JsonObject();\n JsonObject inlineData = new JsonObject();\n inlineData.addProperty(\"mimeType\", \"image/png\");\n inlineData.addProperty(\"data\", base64Png);\n image.add(\"inlineData\", inlineData);\n parts.add(image);\n JsonObject text = new JsonObject();\n text.addProperty(\"text\", prompt);\n parts.add(text);\n content.add(\"parts\", parts);\n contents.add(content);\n root.add(\"contents\", contents);\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0);\n generationConfig.addProperty(\"maxOutputTokens\", MAX_OCR_OUTPUT_TOKENS);\n root.add(\"generationConfig\", generationConfig);\n return root;\n }\n\n private ActiveProfile activeProfile() {\n boolean reuse = ModConfig.OCR_USE_TRANSLATION_MODEL.get();\n if (reuse) {\n ActiveProfile translationProfile = activeProfileFromConfig(true);\n if (translationProfile.ready()) {\n return translationProfile;\n }\n ActiveProfile dedicatedProfile = activeProfileFromConfig(false);\n if (dedicatedProfile.ready()) {\n return dedicatedProfile;\n }\n return ActiveProfile.error(translationProfile.format(),\n translationProfile.errorMessage() + \"; OCR dedicated model: \" + dedicatedProfile.errorMessage());\n }\n return activeProfileFromConfig(false);\n }\n\n private ActiveProfile activeProfileFromConfig(boolean reuse) {\n ModConfig.ApiFormat format = reuse ? ModConfig.API_FORMAT.get() : ModConfig.OCR_API_FORMAT.get();\n String url = reuse ? ModConfig.DEEPSEEK_API_URL.get() : ModConfig.OCR_API_URL.get();\n String key = reuse ? ModConfig.DEEPSEEK_API_KEY.get() : ModConfig.OCR_API_KEY.get();\n String model = reuse ? ModConfig.DEEPSEEK_MODEL.get() : ModConfig.OCR_MODEL.get();\n return activeProfileFromValues(format, url, key, model);\n }\n\n private ActiveProfile activeProfileFromValues(ModConfig.ApiFormat format, String apiUrl,\n String apiKey, String configuredModel) {\n if (!isVisionFormat(format)) {\n return ActiveProfile.error(format, \"OCR model format does not support image input\");\n }\n String key = sanitizeApiKey(apiKey);\n if (key.isBlank()) {\n return ActiveProfile.error(format, \"OCR API key not configured\");\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return ActiveProfile.error(format, urlError);\n }\n String url = ModConfig.normalizeApiUrl(apiUrl);\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? normalizedTranslationModel(format, configuredModel)\n : normalizedOcrModel(format, configuredModel);\n if (model.isBlank()) {\n return ActiveProfile.error(format, \"OCR model not configured\");\n }\n EndpointKind kind = endpointKind(format);\n String endpoint = resolveEndpoint(url, kind, model);\n return new ActiveProfile(true, \"\", format, kind, url, endpoint, key, model);\n }\n\n private static boolean isValidOcrProbeResponse(String raw) {\n if (raw == null || raw.isBlank() || raw.startsWith(OCR_ERROR_PREFIX)) {\n return false;\n }\n String json = objectSlice(stripFences(ModelOutputSanitizer.sanitize(raw)));\n if (json.isBlank()) {\n return false;\n }\n try {\n JsonObject object = JsonParser.parseString(json).getAsJsonObject();\n return object.has(\"sourceText\") && object.has(\"translationText\");\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(Locale.ROOT);\n return trimmed.startsWith(\" resolveChatCompletionsEndpoint(normalized);\n case RESPONSES -> resolveResponsesEndpoint(normalized);\n case ANTHROPIC_MESSAGES -> resolveAnthropicMessagesEndpoint(normalized);\n case GEMINI_GENERATE_CONTENT -> resolveGeminiGenerateContentEndpoint(normalized, model);\n };\n }\n\n private static String resolveChatCompletionsEndpoint(String normalized) {\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private static String resolveResponsesEndpoint(String normalized) {\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 static String resolveAnthropicMessagesEndpoint(String normalized) {\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 static String resolveGeminiGenerateContentEndpoint(String normalized, String model) {\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 void applyAuthHeader(HttpRequest.Builder builder, ActiveProfile profile) {\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", profile.apiKey());\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + profile.apiKey());\n }\n }\n\n private String withApiKeyQuery(ActiveProfile profile) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(profile.apiKey(), StandardCharsets.UTF_8);\n }\n\n private static String extractAssistantContent(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return \"\";\n }\n try {\n JsonObject response = JsonParser.parseString(responseBody).getAsJsonObject();\n String extracted = extractAssistantContent(response);\n return extracted == null ? \"\" : extracted;\n } catch (Exception ignored) {\n return responseBody;\n }\n }\n\n private static String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return \"\";\n }\n if (response.has(\"sourceText\") || response.has(\"translationText\")) {\n return response.toString();\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 String content = textFromJsonContent(message.get(\"content\"));\n if (!content.isBlank()) {\n return content;\n }\n }\n }\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 String text = textFromOutputArray(response.getAsJsonArray(\"output\"));\n if (!text.isBlank()) {\n return text;\n }\n }\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n String text = textFromJsonContent(response.get(\"content\"));\n if (!text.isBlank()) {\n return text;\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\")) {\n return textFromJsonContent(content.get(\"parts\"));\n }\n }\n }\n }\n return \"\";\n }\n\n private static String textFromOutputArray(JsonArray output) {\n StringBuilder builder = new StringBuilder();\n for (JsonElement element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject object = element.getAsJsonObject();\n if (object.has(\"content\")) {\n String content = textFromJsonContent(object.get(\"content\"));\n if (!content.isBlank()) {\n if (!builder.isEmpty()) {\n builder.append('\\n');\n }\n builder.append(content);\n }\n }\n }\n return builder.toString();\n }\n\n private static String textFromJsonContent(JsonElement element) {\n if (element == null || element.isJsonNull()) {\n return \"\";\n }\n if (element.isJsonPrimitive()) {\n return element.getAsString();\n }\n if (!element.isJsonArray()) {\n return \"\";\n }\n StringBuilder builder = new StringBuilder();\n for (JsonElement item : element.getAsJsonArray()) {\n if (!item.isJsonObject()) {\n continue;\n }\n JsonObject part = item.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n if (!builder.isEmpty()) {\n builder.append('\\n');\n }\n builder.append(part.get(\"text\").getAsString());\n }\n }\n return builder.toString();\n }\n\n private static OcrResult parseOcrResult(String raw) {\n if (raw != null && raw.startsWith(OCR_ERROR_PREFIX)) {\n return OcrResult.failure(raw.substring(OCR_ERROR_PREFIX.length()).trim());\n }\n String cleaned = stripFences(ModelOutputSanitizer.sanitize(raw));\n if (cleaned == null || cleaned.isBlank()) {\n return OcrResult.success(\"\", \"\");\n }\n String json = objectSlice(cleaned);\n if (!json.isBlank()) {\n try {\n JsonObject object = JsonParser.parseString(json).getAsJsonObject();\n String source = stringField(object, \"sourceText\");\n String translation = stringField(object, \"translationText\");\n List regions = parseRegions(object.get(\"regions\"));\n return OcrResult.success(source, translation, regions);\n } catch (Exception ignored) {\n // Fall through to non-JSON recovery.\n }\n }\n return OcrResult.success(\"\", cleaned);\n }\n\n private static String stripFences(String value) {\n if (value == null) {\n return \"\";\n }\n String text = value.trim();\n if (text.startsWith(\"```\")) {\n text = text.replaceFirst(\"(?is)^```(?:json|text)?\\\\s*\", \"\");\n text = text.replaceFirst(\"(?is)\\\\s*```$\", \"\");\n }\n return text.trim();\n }\n\n private static String objectSlice(String value) {\n int start = value == null ? -1 : value.indexOf('{');\n int end = value == null ? -1 : value.lastIndexOf('}');\n if (start < 0 || end <= start) {\n return \"\";\n }\n return value.substring(start, end + 1);\n }\n\n private static String stringField(JsonObject object, String key) {\n if (object == null || !object.has(key) || object.get(key).isJsonNull()) {\n return \"\";\n }\n try {\n return object.get(key).getAsString();\n } catch (Exception ignored) {\n return \"\";\n }\n }\n\n private static List parseRegions(JsonElement element) {\n if (element == null || !element.isJsonArray()) {\n return List.of();\n }\n List regions = new ArrayList<>();\n for (JsonElement item : element.getAsJsonArray()) {\n if (!item.isJsonObject()) {\n continue;\n }\n JsonObject object = item.getAsJsonObject();\n String source = firstStringField(object, \"sourceText\", \"source\", \"text\");\n String translation = firstStringField(object, \"translationText\", \"translation\", \"translatedText\");\n JsonArray box = firstArrayField(object, \"bbox\", \"box\", \"bounds\");\n int x = coordinate(object, box, 0, \"x\", \"left\");\n int y = coordinate(object, box, 1, \"y\", \"top\");\n int width = coordinate(object, box, 2, \"width\", \"w\");\n int height = coordinate(object, box, 3, \"height\", \"h\");\n if (width <= 0 || height <= 0 || translation.isBlank()) {\n continue;\n }\n regions.add(new OcrRegion(source, translation, x, y, width, height).sanitized());\n }\n return regions;\n }\n\n private static String firstStringField(JsonObject object, String... keys) {\n for (String key : keys) {\n String value = stringField(object, key);\n if (!value.isBlank()) {\n return value;\n }\n }\n return \"\";\n }\n\n private static JsonArray firstArrayField(JsonObject object, String... keys) {\n for (String key : keys) {\n if (object.has(key) && object.get(key).isJsonArray()) {\n return object.getAsJsonArray(key);\n }\n }\n return null;\n }\n\n private static int coordinate(JsonObject object, JsonArray box, int boxIndex, String... keys) {\n JsonElement value = null;\n for (String key : keys) {\n if (object.has(key) && !object.get(key).isJsonNull()) {\n value = object.get(key);\n break;\n }\n }\n if (value == null && box != null && box.size() > boxIndex) {\n value = box.get(boxIndex);\n }\n if (value == null || value.isJsonNull()) {\n return 0;\n }\n try {\n double number = value.getAsDouble();\n if (number > 0.0 && number <= 1.0) {\n number *= 1000.0;\n }\n return (int) Math.round(number);\n } catch (Exception ignored) {\n return 0;\n }\n }\n\n private static String sanitizeApiKey(String apiKey) {\n if (apiKey == null) {\n return \"\";\n }\n return apiKey.trim();\n }\n\n private static String dataUrl(String base64Png) {\n return \"data:image/png;base64,\" + (base64Png == null ? \"\" : base64Png);\n }\n\n private static String failurePayload(String message) {\n String compact = message == null || message.isBlank() ? \"OCR request failed\" : message.trim();\n compact = compact.replace('\\n', ' ').replace('\\r', ' ');\n return OCR_ERROR_PREFIX + (compact.length() > 240 ? compact.substring(0, 237) + \"...\" : compact);\n }\n\n private static String compactError(String body) {\n if (body == null || body.isBlank()) {\n return \"\";\n }\n String compact = body.replace('\\n', ' ').replace('\\r', ' ').trim();\n return compact.length() > 240 ? compact.substring(0, 237) + \"...\" : compact;\n }\n\n private record ActiveProfile(\n boolean ready,\n String errorMessage,\n ModConfig.ApiFormat format,\n EndpointKind kind,\n String endpointBase,\n String endpointUrl,\n String apiKey,\n String model) {\n static ActiveProfile error(ModConfig.ApiFormat format, String message) {\n return new ActiveProfile(false, message == null ? \"OCR not configured\" : message,\n format == null ? ModConfig.ApiFormat.OPENAI_RESPONSES : format,\n EndpointKind.RESPONSES, \"\", \"\", \"\", \"\");\n }\n\n String queueIdentity() {\n return (format == null ? \"\" : format.name().toLowerCase(Locale.ROOT))\n + \"|\" + endpointBase + \"|\" + endpointUrl + \"|\" + model;\n }\n }\n}\n"}}, "neoforge/SimpleTranslate-Forge-1.20.1/src/main/java/com/yourname/simpletranslate/translation/VisionOcrTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067438.1967628, "n": 34258, "h": "6b065070055b19c4", "c": "package com.yourname.simpletranslate.translation;\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.config.ModConfig;\nimport com.yourname.simpletranslate.util.ModelOutputSanitizer;\nimport com.yourname.simpletranslate.util.TranslationCacheKeys;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URLEncoder;\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.Base64;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.concurrent.CompletableFuture;\n\npublic final class VisionOcrTranslationService implements OcrTranslationService {\n private static final int MAX_OCR_OUTPUT_TOKENS = 2048;\n private static final String OCR_ERROR_PREFIX = \"\\u0000simpletranslate-ocr-error:\";\n private static final String OCR_PROBE_PNG_BASE64 =\n \"iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFnSURBVGhD7ZbRccMgEEQpj4Ioh17Uijohg+XYxwLHnp1kJj7eDD/Ia+BxIIXinIAd3tgCsMMbWwB2eGMLwA5vbAHY4Q2TgDPHEkKAFks+8Zc9w2zMZRQ9Eo7Rt8gMSkAKOEoaTIJZDJNNByQIAbeGwRcgBKwXMJ8Qm01FJmkBZPVpLAW0pTsY8EjNpKSDNtsuEuXIkn4KwMyF/N93j8JCwFlyVBb/jZTwMCCz44WUM5eoHgE99wcCxC7hLBvEYh93AZvtWQmQFWD86w5dgMF0N2lDFuHvgLEgC/9awLu7X6EF6GU8OAJ0tkcXoNxFL6AL+O1L8H5PYIV01VSRQn9o9ysLAfgq6wfWnlteg7JKhgIqjQRlQwwsBXQT1RraobPtYqYCrofP3PTrk4cQUCEWMp3MOoveVAFwR+DxsUIKuANffVfjShGPyq1NpK0EoFQUaMEm4APZArDDG1sAdnhjC8AOb7gX8AW1uKq6OGly1gAAAABJRU5ErkJggg==\";\n private final HttpClient httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n private final Gson gson = new Gson();\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n @Override\n public CompletableFuture translateImage(byte[] pngBytes, String imageHash,\n String sourceLanguage, String targetLanguage) {\n if (pngBytes == null || pngBytes.length == 0) {\n return CompletableFuture.completedFuture(OcrResult.failure(\"No screenshot data\"));\n }\n ActiveProfile profile = activeProfile();\n if (!profile.ready()) {\n return CompletableFuture.completedFuture(OcrResult.failure(profile.errorMessage()));\n }\n String source = sourceLanguage == null || sourceLanguage.isBlank() ? \"auto\" : sourceLanguage;\n String target = targetLanguage == null || targetLanguage.isBlank() ? \"zh_cn\" : targetLanguage;\n String prompt = buildOcrPrompt(source, target);\n String base64 = Base64.getEncoder().encodeToString(pngBytes);\n String queueKey = \"ocr:\" + TranslationCacheKeys.hashSource(profile.queueIdentity()\n + \"\\u0001\" + prompt + \"\\u0001\" + (imageHash == null ? \"\" : imageHash)\n + \"\\u0001\" + base64.length());\n return TranslationRequestQueue.submit(\n queueKey,\n SURFACE,\n TranslationRequestQueue.Priority.INTERACTIVE,\n 2,\n () -> sendOcrRequestBlocking(profile, prompt, base64))\n .thenApply(raw -> raw == null\n ? OcrResult.failure(\"OCR request failed after retries\")\n : parseOcrResult(raw));\n }\n\n @Override\n public CompletableFuture verifyAccess() {\n return translateImage(Base64.getDecoder().decode(OCR_PROBE_PNG_BASE64),\n \"ocr-vision-probe\", \"auto\", ModConfig.TARGET_LANGUAGE.get());\n }\n\n public CompletableFuture verifyAccess(boolean reuseTranslationModel,\n ModConfig.ApiFormat format,\n String apiUrl,\n String apiKey,\n String model) {\n ActiveProfile profile = reuseTranslationModel\n ? activeProfileFromConfig(true)\n : activeProfileFromValues(format, apiUrl, apiKey, model);\n if (!profile.ready()) {\n return CompletableFuture.completedFuture(OcrResult.failure(profile.errorMessage()));\n }\n String prompt = buildOcrPrompt(\"auto\", ModConfig.TARGET_LANGUAGE.get());\n String base64 = OCR_PROBE_PNG_BASE64;\n String queueKey = \"ocr-probe:\" + TranslationCacheKeys.hashSource(profile.queueIdentity());\n return TranslationRequestQueue.submit(\n queueKey,\n SURFACE + \".probe\",\n TranslationRequestQueue.Priority.INTERACTIVE,\n 1,\n () -> sendOcrRequestBlocking(profile, prompt, base64))\n .thenApply(raw -> isValidOcrProbeResponse(raw)\n ? parseOcrResult(raw)\n : OcrResult.failure(\"OCR model did not return the required JSON fields\"));\n }\n\n @Override\n public boolean isReady() {\n return activeProfile().ready();\n }\n\n @Override\n public String describeActiveProfile() {\n ActiveProfile profile = activeProfile();\n if (!profile.ready()) {\n return profile.errorMessage();\n }\n return profile.format().getDisplayName() + \" / \" + profile.model();\n }\n\n public static String buildOcrPrompt(String sourceLanguage, String targetLanguage) {\n String target = targetLanguage == null || targetLanguage.isBlank() ? \"zh_cn\" : targetLanguage;\n String source = sourceLanguage == null || sourceLanguage.isBlank() ? \"auto\" : sourceLanguage;\n return \"\"\"\n You are an OCR and translation engine for Minecraft screenshots.\n Read only visible text inside the image, in natural reading order.\n Ignore Minecraft world objects, cursor/crosshair, panels, borders, and non-text graphics.\n Source language: %s. Target language: %s.\n Return strict JSON only, with exactly:\n {\"sourceText\":\"all source text\",\"translationText\":\"all translated text\",\"regions\":[{\"sourceText\":\"one source block\",\"translationText\":\"its translation\",\"x\":0,\"y\":0,\"width\":1000,\"height\":1000}]}\n regions must contain one entry per visible text line. Do not merge separate lines or distant labels.\n x, y, width, and height are integers normalized to 0..1000 relative to the image.\n Each region must be the tight bounding rectangle of the source glyph pixels, not a loose panel,\n paragraph, translated-text estimate, or surrounding UI container.\n Keep regions in natural reading order. Every translation must correspond only to the source text\n inside that exact region.\n If the image contains no readable text, return {\"sourceText\":\"\",\"translationText\":\"\",\"regions\":[]}.\n Do not add markdown, comments, explanations, or extra fields.\n \"\"\".formatted(source, target);\n }\n\n public static JsonObject buildRequestBodyForTest(ModConfig.ApiFormat format, String model,\n String prompt, String base64Png) {\n EndpointKind kind = endpointKind(format);\n return buildRequestBody(kind, model == null || model.isBlank() ? format.getDefaultModel() : model,\n prompt == null ? \"\" : prompt, base64Png == null ? \"\" : base64Png);\n }\n\n public static OcrResult parseOcrResultForTest(String raw) {\n return parseOcrResult(raw);\n }\n\n private String sendOcrRequestBlocking(ActiveProfile profile, String prompt, String base64Png) {\n try {\n JsonObject body = buildRequestBody(profile.kind(), profile.model(), prompt, base64Png);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile)))\n .timeout(Duration.ofSeconds(60))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(body), StandardCharsets.UTF_8));\n applyAuthHeader(builder, profile);\n HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));\n if (response.statusCode() == 200) {\n if (looksLikeHtml(response.body())) {\n return failurePayload(\"OCR endpoint returned an HTML page; check API URL and interface format\");\n }\n String content = extractAssistantContent(response.body());\n return content == null || content.isBlank()\n ? failurePayload(\"OCR API returned an empty response\")\n : content;\n }\n String message = compactError(response.body());\n if (response.statusCode() == 429 || response.statusCode() >= 500) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (message.isBlank() ? \"\" : \": \" + message));\n }\n SimpleTranslateMod.getLogger().warn(\"OCR vision API error status={} message={}\",\n response.statusCode(), message);\n return failurePayload(\"HTTP \" + response.statusCode() + (message.isBlank() ? \"\" : \": \" + message));\n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n return failurePayload(\"OCR request interrupted\");\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"OCR I/O error\" : e.getMessage(), e);\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n throw e;\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().warn(\"OCR vision request failed: {}\",\n e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());\n return failurePayload(e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());\n }\n }\n\n private static JsonObject buildRequestBody(EndpointKind kind, String model, String prompt, String base64Png) {\n return switch (kind) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(model, prompt, base64Png);\n case RESPONSES -> buildResponsesBody(model, prompt, base64Png);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, prompt, base64Png);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(prompt, base64Png);\n };\n }\n\n private static JsonObject buildChatCompletionsBody(String model, String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n root.addProperty(\"model\", model);\n root.addProperty(\"temperature\", 0);\n root.addProperty(\"max_tokens\", MAX_OCR_OUTPUT_TOKENS);\n\n JsonArray messages = new JsonArray();\n JsonObject user = new JsonObject();\n user.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject text = new JsonObject();\n text.addProperty(\"type\", \"text\");\n text.addProperty(\"text\", prompt);\n content.add(text);\n JsonObject image = new JsonObject();\n image.addProperty(\"type\", \"image_url\");\n JsonObject imageUrl = new JsonObject();\n imageUrl.addProperty(\"url\", dataUrl(base64Png));\n imageUrl.addProperty(\"detail\", \"high\");\n image.add(\"image_url\", imageUrl);\n content.add(image);\n user.add(\"content\", content);\n messages.add(user);\n root.add(\"messages\", messages);\n return root;\n }\n\n private static JsonObject buildResponsesBody(String model, String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n root.addProperty(\"model\", model);\n root.addProperty(\"max_output_tokens\", MAX_OCR_OUTPUT_TOKENS);\n\n JsonArray input = new JsonArray();\n JsonObject user = new JsonObject();\n user.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject text = new JsonObject();\n text.addProperty(\"type\", \"input_text\");\n text.addProperty(\"text\", prompt);\n content.add(text);\n JsonObject image = new JsonObject();\n image.addProperty(\"type\", \"input_image\");\n image.addProperty(\"image_url\", dataUrl(base64Png));\n image.addProperty(\"detail\", \"high\");\n content.add(image);\n user.add(\"content\", content);\n input.add(user);\n root.add(\"input\", input);\n return root;\n }\n\n private static JsonObject buildAnthropicMessagesBody(String model, String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n root.addProperty(\"model\", model);\n root.addProperty(\"max_tokens\", MAX_OCR_OUTPUT_TOKENS);\n root.addProperty(\"temperature\", 0);\n root.addProperty(\"system\", \"Return strict JSON only.\");\n\n JsonArray messages = new JsonArray();\n JsonObject user = new JsonObject();\n user.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject image = new JsonObject();\n image.addProperty(\"type\", \"image\");\n JsonObject source = new JsonObject();\n source.addProperty(\"type\", \"base64\");\n source.addProperty(\"media_type\", \"image/png\");\n source.addProperty(\"data\", base64Png);\n image.add(\"source\", source);\n content.add(image);\n JsonObject text = new JsonObject();\n text.addProperty(\"type\", \"text\");\n text.addProperty(\"text\", prompt);\n content.add(text);\n user.add(\"content\", content);\n messages.add(user);\n root.add(\"messages\", messages);\n return root;\n }\n\n private static JsonObject buildGeminiGenerateContentBody(String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n JsonArray parts = new JsonArray();\n JsonObject image = new JsonObject();\n JsonObject inlineData = new JsonObject();\n inlineData.addProperty(\"mimeType\", \"image/png\");\n inlineData.addProperty(\"data\", base64Png);\n image.add(\"inlineData\", inlineData);\n parts.add(image);\n JsonObject text = new JsonObject();\n text.addProperty(\"text\", prompt);\n parts.add(text);\n content.add(\"parts\", parts);\n contents.add(content);\n root.add(\"contents\", contents);\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0);\n generationConfig.addProperty(\"maxOutputTokens\", MAX_OCR_OUTPUT_TOKENS);\n root.add(\"generationConfig\", generationConfig);\n return root;\n }\n\n private ActiveProfile activeProfile() {\n boolean reuse = ModConfig.OCR_USE_TRANSLATION_MODEL.get();\n if (reuse) {\n ActiveProfile translationProfile = activeProfileFromConfig(true);\n if (translationProfile.ready()) {\n return translationProfile;\n }\n ActiveProfile dedicatedProfile = activeProfileFromConfig(false);\n if (dedicatedProfile.ready()) {\n return dedicatedProfile;\n }\n return ActiveProfile.error(translationProfile.format(),\n translationProfile.errorMessage() + \"; OCR dedicated model: \" + dedicatedProfile.errorMessage());\n }\n return activeProfileFromConfig(false);\n }\n\n private ActiveProfile activeProfileFromConfig(boolean reuse) {\n ModConfig.ApiFormat format = reuse ? ModConfig.API_FORMAT.get() : ModConfig.OCR_API_FORMAT.get();\n String url = reuse ? ModConfig.DEEPSEEK_API_URL.get() : ModConfig.OCR_API_URL.get();\n String key = reuse ? ModConfig.DEEPSEEK_API_KEY.get() : ModConfig.OCR_API_KEY.get();\n String model = reuse ? ModConfig.DEEPSEEK_MODEL.get() : ModConfig.OCR_MODEL.get();\n return activeProfileFromValues(format, url, key, model);\n }\n\n private ActiveProfile activeProfileFromValues(ModConfig.ApiFormat format, String apiUrl,\n String apiKey, String configuredModel) {\n if (!isVisionFormat(format)) {\n return ActiveProfile.error(format, \"OCR model format does not support image input\");\n }\n String key = sanitizeApiKey(apiKey);\n if (key.isBlank()) {\n return ActiveProfile.error(format, \"OCR API key not configured\");\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return ActiveProfile.error(format, urlError);\n }\n String url = ModConfig.normalizeApiUrl(apiUrl);\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? normalizedTranslationModel(format, configuredModel)\n : normalizedOcrModel(format, configuredModel);\n if (model.isBlank()) {\n return ActiveProfile.error(format, \"OCR model not configured\");\n }\n EndpointKind kind = endpointKind(format);\n String endpoint = resolveEndpoint(url, kind, model);\n return new ActiveProfile(true, \"\", format, kind, url, endpoint, key, model);\n }\n\n private static boolean isValidOcrProbeResponse(String raw) {\n if (raw == null || raw.isBlank() || raw.startsWith(OCR_ERROR_PREFIX)) {\n return false;\n }\n String json = objectSlice(stripFences(ModelOutputSanitizer.sanitize(raw)));\n if (json.isBlank()) {\n return false;\n }\n try {\n JsonObject object = JsonParser.parseString(json).getAsJsonObject();\n return object.has(\"sourceText\") && object.has(\"translationText\");\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(Locale.ROOT);\n return trimmed.startsWith(\" resolveChatCompletionsEndpoint(normalized);\n case RESPONSES -> resolveResponsesEndpoint(normalized);\n case ANTHROPIC_MESSAGES -> resolveAnthropicMessagesEndpoint(normalized);\n case GEMINI_GENERATE_CONTENT -> resolveGeminiGenerateContentEndpoint(normalized, model);\n };\n }\n\n private static String resolveChatCompletionsEndpoint(String normalized) {\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private static String resolveResponsesEndpoint(String normalized) {\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 static String resolveAnthropicMessagesEndpoint(String normalized) {\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 static String resolveGeminiGenerateContentEndpoint(String normalized, String model) {\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 void applyAuthHeader(HttpRequest.Builder builder, ActiveProfile profile) {\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", profile.apiKey());\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + profile.apiKey());\n }\n }\n\n private String withApiKeyQuery(ActiveProfile profile) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(profile.apiKey(), StandardCharsets.UTF_8);\n }\n\n private static String extractAssistantContent(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return \"\";\n }\n try {\n JsonObject response = JsonParser.parseString(responseBody).getAsJsonObject();\n String extracted = extractAssistantContent(response);\n return extracted == null ? \"\" : extracted;\n } catch (Exception ignored) {\n return responseBody;\n }\n }\n\n private static String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return \"\";\n }\n if (response.has(\"sourceText\") || response.has(\"translationText\")) {\n return response.toString();\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 String content = textFromJsonContent(message.get(\"content\"));\n if (!content.isBlank()) {\n return content;\n }\n }\n }\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 String text = textFromOutputArray(response.getAsJsonArray(\"output\"));\n if (!text.isBlank()) {\n return text;\n }\n }\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n String text = textFromJsonContent(response.get(\"content\"));\n if (!text.isBlank()) {\n return text;\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\")) {\n return textFromJsonContent(content.get(\"parts\"));\n }\n }\n }\n }\n return \"\";\n }\n\n private static String textFromOutputArray(JsonArray output) {\n StringBuilder builder = new StringBuilder();\n for (JsonElement element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject object = element.getAsJsonObject();\n if (object.has(\"content\")) {\n String content = textFromJsonContent(object.get(\"content\"));\n if (!content.isBlank()) {\n if (!builder.isEmpty()) {\n builder.append('\\n');\n }\n builder.append(content);\n }\n }\n }\n return builder.toString();\n }\n\n private static String textFromJsonContent(JsonElement element) {\n if (element == null || element.isJsonNull()) {\n return \"\";\n }\n if (element.isJsonPrimitive()) {\n return element.getAsString();\n }\n if (!element.isJsonArray()) {\n return \"\";\n }\n StringBuilder builder = new StringBuilder();\n for (JsonElement item : element.getAsJsonArray()) {\n if (!item.isJsonObject()) {\n continue;\n }\n JsonObject part = item.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n if (!builder.isEmpty()) {\n builder.append('\\n');\n }\n builder.append(part.get(\"text\").getAsString());\n }\n }\n return builder.toString();\n }\n\n private static OcrResult parseOcrResult(String raw) {\n if (raw != null && raw.startsWith(OCR_ERROR_PREFIX)) {\n return OcrResult.failure(raw.substring(OCR_ERROR_PREFIX.length()).trim());\n }\n String cleaned = stripFences(ModelOutputSanitizer.sanitize(raw));\n if (cleaned == null || cleaned.isBlank()) {\n return OcrResult.success(\"\", \"\");\n }\n String json = objectSlice(cleaned);\n if (!json.isBlank()) {\n try {\n JsonObject object = JsonParser.parseString(json).getAsJsonObject();\n String source = stringField(object, \"sourceText\");\n String translation = stringField(object, \"translationText\");\n List regions = parseRegions(object.get(\"regions\"));\n return OcrResult.success(source, translation, regions);\n } catch (Exception ignored) {\n // Fall through to non-JSON recovery.\n }\n }\n return OcrResult.success(\"\", cleaned);\n }\n\n private static String stripFences(String value) {\n if (value == null) {\n return \"\";\n }\n String text = value.trim();\n if (text.startsWith(\"```\")) {\n text = text.replaceFirst(\"(?is)^```(?:json|text)?\\\\s*\", \"\");\n text = text.replaceFirst(\"(?is)\\\\s*```$\", \"\");\n }\n return text.trim();\n }\n\n private static String objectSlice(String value) {\n int start = value == null ? -1 : value.indexOf('{');\n int end = value == null ? -1 : value.lastIndexOf('}');\n if (start < 0 || end <= start) {\n return \"\";\n }\n return value.substring(start, end + 1);\n }\n\n private static String stringField(JsonObject object, String key) {\n if (object == null || !object.has(key) || object.get(key).isJsonNull()) {\n return \"\";\n }\n try {\n return object.get(key).getAsString();\n } catch (Exception ignored) {\n return \"\";\n }\n }\n\n private static List parseRegions(JsonElement element) {\n if (element == null || !element.isJsonArray()) {\n return List.of();\n }\n List regions = new ArrayList<>();\n for (JsonElement item : element.getAsJsonArray()) {\n if (!item.isJsonObject()) {\n continue;\n }\n JsonObject object = item.getAsJsonObject();\n String source = firstStringField(object, \"sourceText\", \"source\", \"text\");\n String translation = firstStringField(object, \"translationText\", \"translation\", \"translatedText\");\n JsonArray box = firstArrayField(object, \"bbox\", \"box\", \"bounds\");\n int x = coordinate(object, box, 0, \"x\", \"left\");\n int y = coordinate(object, box, 1, \"y\", \"top\");\n int width = coordinate(object, box, 2, \"width\", \"w\");\n int height = coordinate(object, box, 3, \"height\", \"h\");\n if (width <= 0 || height <= 0 || translation.isBlank()) {\n continue;\n }\n regions.add(new OcrRegion(source, translation, x, y, width, height).sanitized());\n }\n return regions;\n }\n\n private static String firstStringField(JsonObject object, String... keys) {\n for (String key : keys) {\n String value = stringField(object, key);\n if (!value.isBlank()) {\n return value;\n }\n }\n return \"\";\n }\n\n private static JsonArray firstArrayField(JsonObject object, String... keys) {\n for (String key : keys) {\n if (object.has(key) && object.get(key).isJsonArray()) {\n return object.getAsJsonArray(key);\n }\n }\n return null;\n }\n\n private static int coordinate(JsonObject object, JsonArray box, int boxIndex, String... keys) {\n JsonElement value = null;\n for (String key : keys) {\n if (object.has(key) && !object.get(key).isJsonNull()) {\n value = object.get(key);\n break;\n }\n }\n if (value == null && box != null && box.size() > boxIndex) {\n value = box.get(boxIndex);\n }\n if (value == null || value.isJsonNull()) {\n return 0;\n }\n try {\n double number = value.getAsDouble();\n if (number > 0.0 && number <= 1.0) {\n number *= 1000.0;\n }\n return (int) Math.round(number);\n } catch (Exception ignored) {\n return 0;\n }\n }\n\n private static String sanitizeApiKey(String apiKey) {\n if (apiKey == null) {\n return \"\";\n }\n return apiKey.trim();\n }\n\n private static String dataUrl(String base64Png) {\n return \"data:image/png;base64,\" + (base64Png == null ? \"\" : base64Png);\n }\n\n private static String failurePayload(String message) {\n String compact = message == null || message.isBlank() ? \"OCR request failed\" : message.trim();\n compact = compact.replace('\\n', ' ').replace('\\r', ' ');\n return OCR_ERROR_PREFIX + (compact.length() > 240 ? compact.substring(0, 237) + \"...\" : compact);\n }\n\n private static String compactError(String body) {\n if (body == null || body.isBlank()) {\n return \"\";\n }\n String compact = body.replace('\\n', ' ').replace('\\r', ' ').trim();\n return compact.length() > 240 ? compact.substring(0, 237) + \"...\" : compact;\n }\n\n private record ActiveProfile(\n boolean ready,\n String errorMessage,\n ModConfig.ApiFormat format,\n EndpointKind kind,\n String endpointBase,\n String endpointUrl,\n String apiKey,\n String model) {\n static ActiveProfile error(ModConfig.ApiFormat format, String message) {\n return new ActiveProfile(false, message == null ? \"OCR not configured\" : message,\n format == null ? ModConfig.ApiFormat.OPENAI_RESPONSES : format,\n EndpointKind.RESPONSES, \"\", \"\", \"\", \"\");\n }\n\n String queueIdentity() {\n return (format == null ? \"\" : format.name().toLowerCase(Locale.ROOT))\n + \"|\" + endpointBase + \"|\" + endpointUrl + \"|\" + model;\n }\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.20.1/src/main/java/com/yourname/simpletranslate/translation/VisionOcrTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067438.2200313, "n": 34258, "h": "6b065070055b19c4", "c": "package com.yourname.simpletranslate.translation;\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.config.ModConfig;\nimport com.yourname.simpletranslate.util.ModelOutputSanitizer;\nimport com.yourname.simpletranslate.util.TranslationCacheKeys;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URLEncoder;\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.Base64;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.concurrent.CompletableFuture;\n\npublic final class VisionOcrTranslationService implements OcrTranslationService {\n private static final int MAX_OCR_OUTPUT_TOKENS = 2048;\n private static final String OCR_ERROR_PREFIX = \"\\u0000simpletranslate-ocr-error:\";\n private static final String OCR_PROBE_PNG_BASE64 =\n \"iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFnSURBVGhD7ZbRccMgEEQpj4Ioh17Uijohg+XYxwLHnp1kJj7eDD/Ia+BxIIXinIAd3tgCsMMbWwB2eGMLwA5vbAHY4Q2TgDPHEkKAFks+8Zc9w2zMZRQ9Eo7Rt8gMSkAKOEoaTIJZDJNNByQIAbeGwRcgBKwXMJ8Qm01FJmkBZPVpLAW0pTsY8EjNpKSDNtsuEuXIkn4KwMyF/N93j8JCwFlyVBb/jZTwMCCz44WUM5eoHgE99wcCxC7hLBvEYh93AZvtWQmQFWD86w5dgMF0N2lDFuHvgLEgC/9awLu7X6EF6GU8OAJ0tkcXoNxFL6AL+O1L8H5PYIV01VSRQn9o9ysLAfgq6wfWnlteg7JKhgIqjQRlQwwsBXQT1RraobPtYqYCrofP3PTrk4cQUCEWMp3MOoveVAFwR+DxsUIKuANffVfjShGPyq1NpK0EoFQUaMEm4APZArDDG1sAdnhjC8AOb7gX8AW1uKq6OGly1gAAAABJRU5ErkJggg==\";\n private final HttpClient httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n private final Gson gson = new Gson();\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n @Override\n public CompletableFuture translateImage(byte[] pngBytes, String imageHash,\n String sourceLanguage, String targetLanguage) {\n if (pngBytes == null || pngBytes.length == 0) {\n return CompletableFuture.completedFuture(OcrResult.failure(\"No screenshot data\"));\n }\n ActiveProfile profile = activeProfile();\n if (!profile.ready()) {\n return CompletableFuture.completedFuture(OcrResult.failure(profile.errorMessage()));\n }\n String source = sourceLanguage == null || sourceLanguage.isBlank() ? \"auto\" : sourceLanguage;\n String target = targetLanguage == null || targetLanguage.isBlank() ? \"zh_cn\" : targetLanguage;\n String prompt = buildOcrPrompt(source, target);\n String base64 = Base64.getEncoder().encodeToString(pngBytes);\n String queueKey = \"ocr:\" + TranslationCacheKeys.hashSource(profile.queueIdentity()\n + \"\\u0001\" + prompt + \"\\u0001\" + (imageHash == null ? \"\" : imageHash)\n + \"\\u0001\" + base64.length());\n return TranslationRequestQueue.submit(\n queueKey,\n SURFACE,\n TranslationRequestQueue.Priority.INTERACTIVE,\n 2,\n () -> sendOcrRequestBlocking(profile, prompt, base64))\n .thenApply(raw -> raw == null\n ? OcrResult.failure(\"OCR request failed after retries\")\n : parseOcrResult(raw));\n }\n\n @Override\n public CompletableFuture verifyAccess() {\n return translateImage(Base64.getDecoder().decode(OCR_PROBE_PNG_BASE64),\n \"ocr-vision-probe\", \"auto\", ModConfig.TARGET_LANGUAGE.get());\n }\n\n public CompletableFuture verifyAccess(boolean reuseTranslationModel,\n ModConfig.ApiFormat format,\n String apiUrl,\n String apiKey,\n String model) {\n ActiveProfile profile = reuseTranslationModel\n ? activeProfileFromConfig(true)\n : activeProfileFromValues(format, apiUrl, apiKey, model);\n if (!profile.ready()) {\n return CompletableFuture.completedFuture(OcrResult.failure(profile.errorMessage()));\n }\n String prompt = buildOcrPrompt(\"auto\", ModConfig.TARGET_LANGUAGE.get());\n String base64 = OCR_PROBE_PNG_BASE64;\n String queueKey = \"ocr-probe:\" + TranslationCacheKeys.hashSource(profile.queueIdentity());\n return TranslationRequestQueue.submit(\n queueKey,\n SURFACE + \".probe\",\n TranslationRequestQueue.Priority.INTERACTIVE,\n 1,\n () -> sendOcrRequestBlocking(profile, prompt, base64))\n .thenApply(raw -> isValidOcrProbeResponse(raw)\n ? parseOcrResult(raw)\n : OcrResult.failure(\"OCR model did not return the required JSON fields\"));\n }\n\n @Override\n public boolean isReady() {\n return activeProfile().ready();\n }\n\n @Override\n public String describeActiveProfile() {\n ActiveProfile profile = activeProfile();\n if (!profile.ready()) {\n return profile.errorMessage();\n }\n return profile.format().getDisplayName() + \" / \" + profile.model();\n }\n\n public static String buildOcrPrompt(String sourceLanguage, String targetLanguage) {\n String target = targetLanguage == null || targetLanguage.isBlank() ? \"zh_cn\" : targetLanguage;\n String source = sourceLanguage == null || sourceLanguage.isBlank() ? \"auto\" : sourceLanguage;\n return \"\"\"\n You are an OCR and translation engine for Minecraft screenshots.\n Read only visible text inside the image, in natural reading order.\n Ignore Minecraft world objects, cursor/crosshair, panels, borders, and non-text graphics.\n Source language: %s. Target language: %s.\n Return strict JSON only, with exactly:\n {\"sourceText\":\"all source text\",\"translationText\":\"all translated text\",\"regions\":[{\"sourceText\":\"one source block\",\"translationText\":\"its translation\",\"x\":0,\"y\":0,\"width\":1000,\"height\":1000}]}\n regions must contain one entry per visible text line. Do not merge separate lines or distant labels.\n x, y, width, and height are integers normalized to 0..1000 relative to the image.\n Each region must be the tight bounding rectangle of the source glyph pixels, not a loose panel,\n paragraph, translated-text estimate, or surrounding UI container.\n Keep regions in natural reading order. Every translation must correspond only to the source text\n inside that exact region.\n If the image contains no readable text, return {\"sourceText\":\"\",\"translationText\":\"\",\"regions\":[]}.\n Do not add markdown, comments, explanations, or extra fields.\n \"\"\".formatted(source, target);\n }\n\n public static JsonObject buildRequestBodyForTest(ModConfig.ApiFormat format, String model,\n String prompt, String base64Png) {\n EndpointKind kind = endpointKind(format);\n return buildRequestBody(kind, model == null || model.isBlank() ? format.getDefaultModel() : model,\n prompt == null ? \"\" : prompt, base64Png == null ? \"\" : base64Png);\n }\n\n public static OcrResult parseOcrResultForTest(String raw) {\n return parseOcrResult(raw);\n }\n\n private String sendOcrRequestBlocking(ActiveProfile profile, String prompt, String base64Png) {\n try {\n JsonObject body = buildRequestBody(profile.kind(), profile.model(), prompt, base64Png);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile)))\n .timeout(Duration.ofSeconds(60))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(body), StandardCharsets.UTF_8));\n applyAuthHeader(builder, profile);\n HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));\n if (response.statusCode() == 200) {\n if (looksLikeHtml(response.body())) {\n return failurePayload(\"OCR endpoint returned an HTML page; check API URL and interface format\");\n }\n String content = extractAssistantContent(response.body());\n return content == null || content.isBlank()\n ? failurePayload(\"OCR API returned an empty response\")\n : content;\n }\n String message = compactError(response.body());\n if (response.statusCode() == 429 || response.statusCode() >= 500) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (message.isBlank() ? \"\" : \": \" + message));\n }\n SimpleTranslateMod.getLogger().warn(\"OCR vision API error status={} message={}\",\n response.statusCode(), message);\n return failurePayload(\"HTTP \" + response.statusCode() + (message.isBlank() ? \"\" : \": \" + message));\n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n return failurePayload(\"OCR request interrupted\");\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"OCR I/O error\" : e.getMessage(), e);\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n throw e;\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().warn(\"OCR vision request failed: {}\",\n e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());\n return failurePayload(e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());\n }\n }\n\n private static JsonObject buildRequestBody(EndpointKind kind, String model, String prompt, String base64Png) {\n return switch (kind) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(model, prompt, base64Png);\n case RESPONSES -> buildResponsesBody(model, prompt, base64Png);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, prompt, base64Png);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(prompt, base64Png);\n };\n }\n\n private static JsonObject buildChatCompletionsBody(String model, String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n root.addProperty(\"model\", model);\n root.addProperty(\"temperature\", 0);\n root.addProperty(\"max_tokens\", MAX_OCR_OUTPUT_TOKENS);\n\n JsonArray messages = new JsonArray();\n JsonObject user = new JsonObject();\n user.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject text = new JsonObject();\n text.addProperty(\"type\", \"text\");\n text.addProperty(\"text\", prompt);\n content.add(text);\n JsonObject image = new JsonObject();\n image.addProperty(\"type\", \"image_url\");\n JsonObject imageUrl = new JsonObject();\n imageUrl.addProperty(\"url\", dataUrl(base64Png));\n imageUrl.addProperty(\"detail\", \"high\");\n image.add(\"image_url\", imageUrl);\n content.add(image);\n user.add(\"content\", content);\n messages.add(user);\n root.add(\"messages\", messages);\n return root;\n }\n\n private static JsonObject buildResponsesBody(String model, String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n root.addProperty(\"model\", model);\n root.addProperty(\"max_output_tokens\", MAX_OCR_OUTPUT_TOKENS);\n\n JsonArray input = new JsonArray();\n JsonObject user = new JsonObject();\n user.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject text = new JsonObject();\n text.addProperty(\"type\", \"input_text\");\n text.addProperty(\"text\", prompt);\n content.add(text);\n JsonObject image = new JsonObject();\n image.addProperty(\"type\", \"input_image\");\n image.addProperty(\"image_url\", dataUrl(base64Png));\n image.addProperty(\"detail\", \"high\");\n content.add(image);\n user.add(\"content\", content);\n input.add(user);\n root.add(\"input\", input);\n return root;\n }\n\n private static JsonObject buildAnthropicMessagesBody(String model, String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n root.addProperty(\"model\", model);\n root.addProperty(\"max_tokens\", MAX_OCR_OUTPUT_TOKENS);\n root.addProperty(\"temperature\", 0);\n root.addProperty(\"system\", \"Return strict JSON only.\");\n\n JsonArray messages = new JsonArray();\n JsonObject user = new JsonObject();\n user.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject image = new JsonObject();\n image.addProperty(\"type\", \"image\");\n JsonObject source = new JsonObject();\n source.addProperty(\"type\", \"base64\");\n source.addProperty(\"media_type\", \"image/png\");\n source.addProperty(\"data\", base64Png);\n image.add(\"source\", source);\n content.add(image);\n JsonObject text = new JsonObject();\n text.addProperty(\"type\", \"text\");\n text.addProperty(\"text\", prompt);\n content.add(text);\n user.add(\"content\", content);\n messages.add(user);\n root.add(\"messages\", messages);\n return root;\n }\n\n private static JsonObject buildGeminiGenerateContentBody(String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n JsonArray parts = new JsonArray();\n JsonObject image = new JsonObject();\n JsonObject inlineData = new JsonObject();\n inlineData.addProperty(\"mimeType\", \"image/png\");\n inlineData.addProperty(\"data\", base64Png);\n image.add(\"inlineData\", inlineData);\n parts.add(image);\n JsonObject text = new JsonObject();\n text.addProperty(\"text\", prompt);\n parts.add(text);\n content.add(\"parts\", parts);\n contents.add(content);\n root.add(\"contents\", contents);\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0);\n generationConfig.addProperty(\"maxOutputTokens\", MAX_OCR_OUTPUT_TOKENS);\n root.add(\"generationConfig\", generationConfig);\n return root;\n }\n\n private ActiveProfile activeProfile() {\n boolean reuse = ModConfig.OCR_USE_TRANSLATION_MODEL.get();\n if (reuse) {\n ActiveProfile translationProfile = activeProfileFromConfig(true);\n if (translationProfile.ready()) {\n return translationProfile;\n }\n ActiveProfile dedicatedProfile = activeProfileFromConfig(false);\n if (dedicatedProfile.ready()) {\n return dedicatedProfile;\n }\n return ActiveProfile.error(translationProfile.format(),\n translationProfile.errorMessage() + \"; OCR dedicated model: \" + dedicatedProfile.errorMessage());\n }\n return activeProfileFromConfig(false);\n }\n\n private ActiveProfile activeProfileFromConfig(boolean reuse) {\n ModConfig.ApiFormat format = reuse ? ModConfig.API_FORMAT.get() : ModConfig.OCR_API_FORMAT.get();\n String url = reuse ? ModConfig.DEEPSEEK_API_URL.get() : ModConfig.OCR_API_URL.get();\n String key = reuse ? ModConfig.DEEPSEEK_API_KEY.get() : ModConfig.OCR_API_KEY.get();\n String model = reuse ? ModConfig.DEEPSEEK_MODEL.get() : ModConfig.OCR_MODEL.get();\n return activeProfileFromValues(format, url, key, model);\n }\n\n private ActiveProfile activeProfileFromValues(ModConfig.ApiFormat format, String apiUrl,\n String apiKey, String configuredModel) {\n if (!isVisionFormat(format)) {\n return ActiveProfile.error(format, \"OCR model format does not support image input\");\n }\n String key = sanitizeApiKey(apiKey);\n if (key.isBlank()) {\n return ActiveProfile.error(format, \"OCR API key not configured\");\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return ActiveProfile.error(format, urlError);\n }\n String url = ModConfig.normalizeApiUrl(apiUrl);\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? normalizedTranslationModel(format, configuredModel)\n : normalizedOcrModel(format, configuredModel);\n if (model.isBlank()) {\n return ActiveProfile.error(format, \"OCR model not configured\");\n }\n EndpointKind kind = endpointKind(format);\n String endpoint = resolveEndpoint(url, kind, model);\n return new ActiveProfile(true, \"\", format, kind, url, endpoint, key, model);\n }\n\n private static boolean isValidOcrProbeResponse(String raw) {\n if (raw == null || raw.isBlank() || raw.startsWith(OCR_ERROR_PREFIX)) {\n return false;\n }\n String json = objectSlice(stripFences(ModelOutputSanitizer.sanitize(raw)));\n if (json.isBlank()) {\n return false;\n }\n try {\n JsonObject object = JsonParser.parseString(json).getAsJsonObject();\n return object.has(\"sourceText\") && object.has(\"translationText\");\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(Locale.ROOT);\n return trimmed.startsWith(\" resolveChatCompletionsEndpoint(normalized);\n case RESPONSES -> resolveResponsesEndpoint(normalized);\n case ANTHROPIC_MESSAGES -> resolveAnthropicMessagesEndpoint(normalized);\n case GEMINI_GENERATE_CONTENT -> resolveGeminiGenerateContentEndpoint(normalized, model);\n };\n }\n\n private static String resolveChatCompletionsEndpoint(String normalized) {\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private static String resolveResponsesEndpoint(String normalized) {\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 static String resolveAnthropicMessagesEndpoint(String normalized) {\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 static String resolveGeminiGenerateContentEndpoint(String normalized, String model) {\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 void applyAuthHeader(HttpRequest.Builder builder, ActiveProfile profile) {\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", profile.apiKey());\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + profile.apiKey());\n }\n }\n\n private String withApiKeyQuery(ActiveProfile profile) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(profile.apiKey(), StandardCharsets.UTF_8);\n }\n\n private static String extractAssistantContent(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return \"\";\n }\n try {\n JsonObject response = JsonParser.parseString(responseBody).getAsJsonObject();\n String extracted = extractAssistantContent(response);\n return extracted == null ? \"\" : extracted;\n } catch (Exception ignored) {\n return responseBody;\n }\n }\n\n private static String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return \"\";\n }\n if (response.has(\"sourceText\") || response.has(\"translationText\")) {\n return response.toString();\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 String content = textFromJsonContent(message.get(\"content\"));\n if (!content.isBlank()) {\n return content;\n }\n }\n }\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 String text = textFromOutputArray(response.getAsJsonArray(\"output\"));\n if (!text.isBlank()) {\n return text;\n }\n }\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n String text = textFromJsonContent(response.get(\"content\"));\n if (!text.isBlank()) {\n return text;\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\")) {\n return textFromJsonContent(content.get(\"parts\"));\n }\n }\n }\n }\n return \"\";\n }\n\n private static String textFromOutputArray(JsonArray output) {\n StringBuilder builder = new StringBuilder();\n for (JsonElement element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject object = element.getAsJsonObject();\n if (object.has(\"content\")) {\n String content = textFromJsonContent(object.get(\"content\"));\n if (!content.isBlank()) {\n if (!builder.isEmpty()) {\n builder.append('\\n');\n }\n builder.append(content);\n }\n }\n }\n return builder.toString();\n }\n\n private static String textFromJsonContent(JsonElement element) {\n if (element == null || element.isJsonNull()) {\n return \"\";\n }\n if (element.isJsonPrimitive()) {\n return element.getAsString();\n }\n if (!element.isJsonArray()) {\n return \"\";\n }\n StringBuilder builder = new StringBuilder();\n for (JsonElement item : element.getAsJsonArray()) {\n if (!item.isJsonObject()) {\n continue;\n }\n JsonObject part = item.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n if (!builder.isEmpty()) {\n builder.append('\\n');\n }\n builder.append(part.get(\"text\").getAsString());\n }\n }\n return builder.toString();\n }\n\n private static OcrResult parseOcrResult(String raw) {\n if (raw != null && raw.startsWith(OCR_ERROR_PREFIX)) {\n return OcrResult.failure(raw.substring(OCR_ERROR_PREFIX.length()).trim());\n }\n String cleaned = stripFences(ModelOutputSanitizer.sanitize(raw));\n if (cleaned == null || cleaned.isBlank()) {\n return OcrResult.success(\"\", \"\");\n }\n String json = objectSlice(cleaned);\n if (!json.isBlank()) {\n try {\n JsonObject object = JsonParser.parseString(json).getAsJsonObject();\n String source = stringField(object, \"sourceText\");\n String translation = stringField(object, \"translationText\");\n List regions = parseRegions(object.get(\"regions\"));\n return OcrResult.success(source, translation, regions);\n } catch (Exception ignored) {\n // Fall through to non-JSON recovery.\n }\n }\n return OcrResult.success(\"\", cleaned);\n }\n\n private static String stripFences(String value) {\n if (value == null) {\n return \"\";\n }\n String text = value.trim();\n if (text.startsWith(\"```\")) {\n text = text.replaceFirst(\"(?is)^```(?:json|text)?\\\\s*\", \"\");\n text = text.replaceFirst(\"(?is)\\\\s*```$\", \"\");\n }\n return text.trim();\n }\n\n private static String objectSlice(String value) {\n int start = value == null ? -1 : value.indexOf('{');\n int end = value == null ? -1 : value.lastIndexOf('}');\n if (start < 0 || end <= start) {\n return \"\";\n }\n return value.substring(start, end + 1);\n }\n\n private static String stringField(JsonObject object, String key) {\n if (object == null || !object.has(key) || object.get(key).isJsonNull()) {\n return \"\";\n }\n try {\n return object.get(key).getAsString();\n } catch (Exception ignored) {\n return \"\";\n }\n }\n\n private static List parseRegions(JsonElement element) {\n if (element == null || !element.isJsonArray()) {\n return List.of();\n }\n List regions = new ArrayList<>();\n for (JsonElement item : element.getAsJsonArray()) {\n if (!item.isJsonObject()) {\n continue;\n }\n JsonObject object = item.getAsJsonObject();\n String source = firstStringField(object, \"sourceText\", \"source\", \"text\");\n String translation = firstStringField(object, \"translationText\", \"translation\", \"translatedText\");\n JsonArray box = firstArrayField(object, \"bbox\", \"box\", \"bounds\");\n int x = coordinate(object, box, 0, \"x\", \"left\");\n int y = coordinate(object, box, 1, \"y\", \"top\");\n int width = coordinate(object, box, 2, \"width\", \"w\");\n int height = coordinate(object, box, 3, \"height\", \"h\");\n if (width <= 0 || height <= 0 || translation.isBlank()) {\n continue;\n }\n regions.add(new OcrRegion(source, translation, x, y, width, height).sanitized());\n }\n return regions;\n }\n\n private static String firstStringField(JsonObject object, String... keys) {\n for (String key : keys) {\n String value = stringField(object, key);\n if (!value.isBlank()) {\n return value;\n }\n }\n return \"\";\n }\n\n private static JsonArray firstArrayField(JsonObject object, String... keys) {\n for (String key : keys) {\n if (object.has(key) && object.get(key).isJsonArray()) {\n return object.getAsJsonArray(key);\n }\n }\n return null;\n }\n\n private static int coordinate(JsonObject object, JsonArray box, int boxIndex, String... keys) {\n JsonElement value = null;\n for (String key : keys) {\n if (object.has(key) && !object.get(key).isJsonNull()) {\n value = object.get(key);\n break;\n }\n }\n if (value == null && box != null && box.size() > boxIndex) {\n value = box.get(boxIndex);\n }\n if (value == null || value.isJsonNull()) {\n return 0;\n }\n try {\n double number = value.getAsDouble();\n if (number > 0.0 && number <= 1.0) {\n number *= 1000.0;\n }\n return (int) Math.round(number);\n } catch (Exception ignored) {\n return 0;\n }\n }\n\n private static String sanitizeApiKey(String apiKey) {\n if (apiKey == null) {\n return \"\";\n }\n return apiKey.trim();\n }\n\n private static String dataUrl(String base64Png) {\n return \"data:image/png;base64,\" + (base64Png == null ? \"\" : base64Png);\n }\n\n private static String failurePayload(String message) {\n String compact = message == null || message.isBlank() ? \"OCR request failed\" : message.trim();\n compact = compact.replace('\\n', ' ').replace('\\r', ' ');\n return OCR_ERROR_PREFIX + (compact.length() > 240 ? compact.substring(0, 237) + \"...\" : compact);\n }\n\n private static String compactError(String body) {\n if (body == null || body.isBlank()) {\n return \"\";\n }\n String compact = body.replace('\\n', ' ').replace('\\r', ' ').trim();\n return compact.length() > 240 ? compact.substring(0, 237) + \"...\" : compact;\n }\n\n private record ActiveProfile(\n boolean ready,\n String errorMessage,\n ModConfig.ApiFormat format,\n EndpointKind kind,\n String endpointBase,\n String endpointUrl,\n String apiKey,\n String model) {\n static ActiveProfile error(ModConfig.ApiFormat format, String message) {\n return new ActiveProfile(false, message == null ? \"OCR not configured\" : message,\n format == null ? ModConfig.ApiFormat.OPENAI_RESPONSES : format,\n EndpointKind.RESPONSES, \"\", \"\", \"\", \"\");\n }\n\n String queueIdentity() {\n return (format == null ? \"\" : format.name().toLowerCase(Locale.ROOT))\n + \"|\" + endpointBase + \"|\" + endpointUrl + \"|\" + model;\n }\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.8/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067439.490665, "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"}}, "fabric/SimpleTranslate-Fabric-1.21.11/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067439.524349, "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"}}, "fabric/SimpleTranslate-Fabric-1.21.9/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067439.6100154, "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"}, "c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068303.8768024, "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.8/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067439.6604788, "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/translation/DeepSeekTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067439.8599145, "n": 60896, "h": "4db9c81e047177fe", "c": "package com.yourname.simpletranslate.translation;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.util.ModelOutputSanitizer;\nimport com.yourname.simpletranslate.util.TranslationCacheKeys;\nimport com.yourname.simpletranslate.util.TranslationMapping;\nimport com.yourname.simpletranslate.util.TranslationMappingResult;\nimport com.yourname.simpletranslate.util.TranslationSlot;\nimport com.yourname.simpletranslate.util.TranslationTextDetector;\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.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 private static final Pattern ALLOWED_MAPPING_PLACEHOLDER_PATTERN = Pattern.compile(\"@@@F\\\\d+@@@\");\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 = 8192;\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 DIRECT_SURFACE_PATTERN = Pattern.compile(\"]*\\\\bsurface=\\\"([^\\\"]+)\\\"\");\n private static final Pattern MAPPING_SURFACE_PATTERN = Pattern.compile(\"\\\"surface\\\"\\\\s*:\\\\s*\\\"([^\\\"]+)\\\"\");\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 /** Alias so existing references keep working after the prompt-builder split. */\n private static final class RequestMode {\n static final TranslationPrompts.RequestMode PLAIN_TEXT = TranslationPrompts.RequestMode.PLAIN_TEXT;\n static final TranslationPrompts.RequestMode PRESERVE_MARKERS = TranslationPrompts.RequestMode.PRESERVE_MARKERS;\n static final TranslationPrompts.RequestMode MAPPING_JSON = TranslationPrompts.RequestMode.MAPPING_JSON;\n static final TranslationPrompts.RequestMode DIRECT_FORMATTED = TranslationPrompts.RequestMode.DIRECT_FORMATTED;\n\n private RequestMode() {\n }\n }\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 @Override\n public CompletableFuture translate(String text, String sourceLanguage, String targetLanguage) {\n return translateWithTerms(text, sourceLanguage, targetLanguage, List.of());\n }\n\n @Override\n public CompletableFuture translateWithTerms(String text, String sourceLanguage, String targetLanguage,\n List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.PLAIN_TEXT);\n return sendRequest(systemPrompt, text);\n }\n\n @Override\n public CompletableFuture translatePreservingMarkers(String text, String sourceLanguage,\n String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.PRESERVE_MARKERS);\n return sendRequest(systemPrompt, text);\n }\n\n @Override\n public CompletableFuture translateFormattedDocument(String document, String surface,\n String sourceLanguage, String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n // Style/term hints live in the user payload so the system prompt stays\n // byte-stable per language pair (provider prefix caching).\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, List.of(),\n RequestMode.DIRECT_FORMATTED);\n String userPrompt = insertDirectSections(document, TranslationPrompts.directUserSections(termHints));\n return sendRequest(systemPrompt, userPrompt, estimateDirectMaxTokens(document), surface);\n }\n\n /** Same as {@link #translateFormattedDocument} but scales the output token budget. */\n public CompletableFuture translateFormattedDocument(String document, String surface,\n String sourceLanguage, String targetLanguage, List termHints, int maxTokenMultiplier) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, List.of(),\n RequestMode.DIRECT_FORMATTED);\n String userPrompt = insertDirectSections(document, TranslationPrompts.directUserSections(termHints));\n return sendRequest(systemPrompt, userPrompt, estimateDirectMaxTokens(document, maxTokenMultiplier), surface);\n }\n\n /** Inserts [STYLE]/[TERMS] sections right before the [TEXT] block. */\n private static String insertDirectSections(String document, String sections) {\n if (sections == null || sections.isBlank() || document == null) {\n return document;\n }\n int index = document.indexOf(\"[TEXT\");\n if (index < 0) {\n return sections + document;\n }\n return document.substring(0, index) + sections + document.substring(index);\n }\n\n /** Output budget follows the [TEXT] block size, not the whole payload with context. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Output budget follows the [TEXT] block; multiplier supports partial retries. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int start = body.indexOf(\"[TEXT\");\n int end = body.lastIndexOf(\"[/TEXT]\");\n if (start >= 0 && end > start) {\n body = body.substring(start, end);\n }\n int lineCount = 0;\n for (String line : body.split(\"\\\\R\")) {\n if (line != null && line.matches(\"(?s).*?\\\\d{1,4}\\\\s*[||].*\")) {\n lineCount++;\n }\n }\n // Chinese game text often expands; multi-line tooltips need headroom per line.\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int lineEstimate = lineCount * 192 + 384;\n int estimated = Math.max(charEstimate, lineEstimate);\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 @Override\n public CompletableFuture translateMapping(TranslationMapping mapping,\n String sourceLanguage, String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.MAPPING_JSON);\n String userPrompt = buildMappingUserPrompt(mapping, sourceLanguage, targetLanguage);\n return sendRequest(systemPrompt, userPrompt).thenApply(raw -> parseMappingResponse(raw, mapping));\n }\n\n @Override\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationService.ApiDetectionResult(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 @Override\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationService.ModelDetectionResult(\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 @Override\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 TranslationService.ModelAccessResult(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationService.ModelAccessResult(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationService.ModelAccessResult(\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 String buildSystemPrompt(String sourceLanguage, String targetLanguage, List termHints,\n TranslationPrompts.RequestMode mode) {\n return TranslationPrompts.buildSystemPrompt(sourceLanguage, targetLanguage, termHints, mode);\n }\n\n\n private String buildMappingUserPrompt(TranslationMapping mapping, String sourceLanguage, String targetLanguage) {\n JsonObject root = new JsonObject();\n root.addProperty(\"version\", \"mapping-v1\");\n root.addProperty(\"jobId\", mapping.taskId());\n root.addProperty(\"surface\", mapping.surface());\n root.addProperty(\"sourceLanguage\", getLanguageName(sourceLanguage));\n root.addProperty(\"targetLanguage\", getLanguageName(targetLanguage));\n root.addProperty(\"context\", mapping.context());\n root.addProperty(\"layoutSignature\", mapping.layoutSignature());\n root.addProperty(\"styleSignature\", mapping.styleSignature());\n boolean flexibleLayout = isFlexibleMappingSurface(mapping.surface());\n root.addProperty(\"layoutMode\", flexibleLayout ? \"flexible-tooltip\" : \"fixed-layout\");\n root.addProperty(\"instructions\", flexibleLayout\n ? \"Translate every unit as its own semantic style block or styled token. Use natural Minecraft localization for the target language, ignore source visual line breaks, keep every @@@F marker exactly once in the same unit, and preserve only structural tokens such as numbers, ids, commands, control codes, and placeholders.\"\n : \"Translate each fixed-layout slot conservatively. Preserve line/slot structure, technical tokens, and every @@@F marker exactly.\");\n\n JsonArray units = new JsonArray();\n for (TranslationSlot slot : mapping.slots()) {\n JsonObject unit = new JsonObject();\n unit.addProperty(\"id\", slot.id());\n unit.addProperty(\"text\", slot.text());\n unit.addProperty(\"role\", slot.role());\n unit.addProperty(\"maxChars\", slot.maxWidth());\n unit.addProperty(\"fixed\", slot.fixedSlot());\n unit.addProperty(\"lineIndex\", slot.lineIndex());\n unit.addProperty(\"slotIndex\", slot.slotIndex());\n unit.addProperty(\"styleSignature\", slot.styleSignature());\n unit.addProperty(\"tokenMask\", slot.tokenMask());\n units.add(unit);\n }\n root.add(\"units\", units);\n return root.toString();\n }\n\n private boolean isFlexibleMappingSurface(String surface) {\n if (surface == null) {\n return false;\n }\n String normalized = surface.toLowerCase();\n return normalized.startsWith(\"tooltip.\")\n || normalized.startsWith(\"hover.\")\n || normalized.startsWith(\"book.\");\n }\n\n private String getLanguageName(String code) {\n return TranslationTextDetector.displayLanguageName(code);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt) {\n return sendRequest(systemPrompt, userPrompt, estimateMaxTokens(userPrompt));\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens) {\n return sendRequest(systemPrompt, userPrompt, maxTokens, null);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String explicitSurface) {\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 surface = explicitSurface == null || explicitSurface.isBlank()\n ? detectRequestSurface(userPrompt)\n : explicitSurface;\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestBlocking(profiles, apiKey, model, systemPrompt, userPrompt, maxTokens));\n }\n\n private String sendRequestBlocking(List profiles, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n for (ApiRequestProfile profile : orderedProfiles) {\n String translated = sendRequestWithProfile(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return translated;\n }\n }\n return null;\n }\n\n private String sendRequestWithProfile(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return null;\n }\n\n long startedAt = System.nanoTime();\n try {\n HttpRequest request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n String result = parseResponse(response.body(), startedAt);\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 return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\"Empty translation result from DeepSeek\");\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 (InterruptedException e) {\n Thread.currentThread().interrupt();\n SimpleTranslateMod.getLogger().error(\"Translation request interrupted\", e);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n throw e;\n } catch (InvalidApiResponseException e) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\", e.getMessage());\n return null;\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\", e.getMessage(), e);\n return null;\n }\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 String detectRequestSurface(String userPrompt) {\n if (userPrompt == null || userPrompt.isBlank()) {\n return \"manager.raw\";\n }\n Matcher direct = DIRECT_SURFACE_PATTERN.matcher(userPrompt);\n if (direct.find()) {\n return direct.group(1);\n }\n Matcher mapping = MAPPING_SURFACE_PATTERN.matcher(userPrompt);\n if (mapping.find()) {\n return mapping.group(1);\n }\n return \"manager.raw\";\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 static int estimateMaxTokens(String text) {\n int length = text == null ? 0 : text.length();\n int estimated = length + 512;\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n return MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n return Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.contains(\"data:\")) {\n return parseStreamingResponse(responseBody);\n }\n return parsePlainResponse(responseBody);\n }\n\n private String parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return null;\n }\n\n Future readerTask = 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 null;\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 String parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return null;\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\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 null;\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);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return null;\n }\n\n if (sawData) {\n return ModelOutputSanitizer.sanitize(result.toString());\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 String parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\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);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return null;\n }\n return ModelOutputSanitizer.sanitize(result.toString());\n }\n\n private String parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\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 ModelOutputSanitizer.sanitize(raw);\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 TranslationMappingResult parseMappingResponse(String response, TranslationMapping mapping) {\n String sanitized = ModelOutputSanitizer.sanitize(response);\n if (sanitized == null || sanitized.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n try {\n String jsonPayload = extractJsonObjectPayload(sanitized);\n if (jsonPayload == null || jsonPayload.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n JsonObject root = JsonParser.parseString(jsonPayload).getAsJsonObject();\n String version = root.has(\"version\") ? root.get(\"version\").getAsString() : \"\";\n String jobId = root.has(\"jobId\") ? root.get(\"jobId\").getAsString() : \"\";\n if (!\"mapping-v1\".equals(version) || !mapping.taskId().equals(jobId)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n if (!root.has(\"translations\") || !root.get(\"translations\").isJsonArray()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n Map translations = new LinkedHashMap<>();\n for (var element : root.getAsJsonArray(\"translations\")) {\n if (!element.isJsonObject()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n JsonObject item = element.getAsJsonObject();\n String id = item.has(\"id\") ? item.get(\"id\").getAsString() : \"\";\n String translation = item.has(\"translation\") ? item.get(\"translation\").getAsString() : \"\";\n if (id.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n String cleaned = ModelOutputSanitizer.sanitize(translation);\n if (cleaned == null || cleaned.isBlank() || containsDisallowedMappingTokens(cleaned)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n translations.put(id, cleaned);\n }\n\n if (translations.size() != mapping.slots().size()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n for (TranslationSlot slot : mapping.slots()) {\n if (!translations.containsKey(slot.id())) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n if (slot.fixedSlot()) {\n String normalizedSource = TranslationCacheKeys.normalizeSource(slot.text());\n String normalizedTranslated = TranslationCacheKeys.normalizeSource(translations.get(slot.id()));\n if (!normalizedSource.equals(normalizedTranslated)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n }\n }\n\n return new TranslationMappingResult(mapping.taskId(), translations);\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().debug(\"Failed to parse mapping response\", e);\n return new TranslationMappingResult(mapping.taskId(), Map.of());\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 boolean containsDisallowedMappingTokens(String text) {\n if (text == null || text.isEmpty()) {\n return false;\n }\n return containsDisallowedMappingMarker(text)\n || text.contains(\"```\")\n || text.contains(\"\")\n || text.contains(\"\")\n || text.contains(\"\")\n || text.contains(\"\");\n }\n\n private boolean containsDisallowedMappingMarker(String text) {\n int index = text.indexOf(\"@@@\");\n if (index < 0) {\n return false;\n }\n Matcher matcher = ALLOWED_MAPPING_PLACEHOLDER_PATTERN.matcher(text);\n int cursor = 0;\n while (matcher.find()) {\n String between = text.substring(cursor, matcher.start());\n if (between.contains(\"@@@\")) {\n return true;\n }\n cursor = matcher.end();\n }\n return text.substring(cursor).contains(\"@@@\");\n }\n\n private void appendStreamingChunk(StringBuilder result, String json) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\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 TranslationService.ApiDetectionResult 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 = sendRequestWithProfile(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationService.ApiDetectionResult(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 TranslationService.ApiDetectionResult(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationService.ModelAccessResult 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 = buildSystemPrompt(\"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(),\n RequestMode.DIRECT_FORMATTED);\n String probeUserPrompt = \"\"\"\n \n [TEXT lines=1]\n 0|Hello\n [/TEXT]\n \n \"\"\";\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.send(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream());\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 TranslationService.ModelAccessResult(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 (InterruptedException e) {\n Thread.currentThread().interrupt();\n return new TranslationService.ModelAccessResult(false, modelId, 0, \"Model verification interrupted\");\n } catch (IOException | RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationService.ModelAccessResult(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n for (String line : value.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (trimmed.matches(\"0\\\\s*[||].+\")) {\n return true;\n }\n }\n return false;\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, false));\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 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(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 requestBody.addProperty(\"max_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n if (!thinkingEnabled) {\n requestBody.addProperty(\"temperature\", 0.1);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\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 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 requestBody.addProperty(\"max_output_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n requestBody.addProperty(\"temperature\", 0.1);\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 requestBody.addProperty(\"max_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n requestBody.addProperty(\"temperature\", 0.1);\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 systemPrompt, String userPrompt, 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.1);\n generationConfig.addProperty(\"maxOutputTokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\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 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 @Override\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n @Override\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.10/src/main/java/com/yourname/simpletranslate/translation/VisionOcrTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067440.3172727, "n": 34258, "h": "6b065070055b19c4", "c": "package com.yourname.simpletranslate.translation;\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.config.ModConfig;\nimport com.yourname.simpletranslate.util.ModelOutputSanitizer;\nimport com.yourname.simpletranslate.util.TranslationCacheKeys;\n\nimport java.io.IOException;\nimport java.net.URI;\nimport java.net.URLEncoder;\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.Base64;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Locale;\nimport java.util.concurrent.CompletableFuture;\n\npublic final class VisionOcrTranslationService implements OcrTranslationService {\n private static final int MAX_OCR_OUTPUT_TOKENS = 2048;\n private static final String OCR_ERROR_PREFIX = \"\\u0000simpletranslate-ocr-error:\";\n private static final String OCR_PROBE_PNG_BASE64 =\n \"iVBORw0KGgoAAAANSUhEUgAAAEAAAAAgCAYAAACinX6EAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAFnSURBVGhD7ZbRccMgEEQpj4Ioh17Uijohg+XYxwLHnp1kJj7eDD/Ia+BxIIXinIAd3tgCsMMbWwB2eGMLwA5vbAHY4Q2TgDPHEkKAFks+8Zc9w2zMZRQ9Eo7Rt8gMSkAKOEoaTIJZDJNNByQIAbeGwRcgBKwXMJ8Qm01FJmkBZPVpLAW0pTsY8EjNpKSDNtsuEuXIkn4KwMyF/N93j8JCwFlyVBb/jZTwMCCz44WUM5eoHgE99wcCxC7hLBvEYh93AZvtWQmQFWD86w5dgMF0N2lDFuHvgLEgC/9awLu7X6EF6GU8OAJ0tkcXoNxFL6AL+O1L8H5PYIV01VSRQn9o9ysLAfgq6wfWnlteg7JKhgIqjQRlQwwsBXQT1RraobPtYqYCrofP3PTrk4cQUCEWMp3MOoveVAFwR+DxsUIKuANffVfjShGPyq1NpK0EoFQUaMEm4APZArDDG1sAdnhjC8AOb7gX8AW1uKq6OGly1gAAAABJRU5ErkJggg==\";\n private final HttpClient httpClient = HttpClient.newBuilder()\n .version(HttpClient.Version.HTTP_1_1)\n .connectTimeout(Duration.ofSeconds(30))\n .build();\n private final Gson gson = new Gson();\n\n private enum EndpointKind {\n CHAT_COMPLETIONS,\n RESPONSES,\n ANTHROPIC_MESSAGES,\n GEMINI_GENERATE_CONTENT\n }\n\n @Override\n public CompletableFuture translateImage(byte[] pngBytes, String imageHash,\n String sourceLanguage, String targetLanguage) {\n if (pngBytes == null || pngBytes.length == 0) {\n return CompletableFuture.completedFuture(OcrResult.failure(\"No screenshot data\"));\n }\n ActiveProfile profile = activeProfile();\n if (!profile.ready()) {\n return CompletableFuture.completedFuture(OcrResult.failure(profile.errorMessage()));\n }\n String source = sourceLanguage == null || sourceLanguage.isBlank() ? \"auto\" : sourceLanguage;\n String target = targetLanguage == null || targetLanguage.isBlank() ? \"zh_cn\" : targetLanguage;\n String prompt = buildOcrPrompt(source, target);\n String base64 = Base64.getEncoder().encodeToString(pngBytes);\n String queueKey = \"ocr:\" + TranslationCacheKeys.hashSource(profile.queueIdentity()\n + \"\\u0001\" + prompt + \"\\u0001\" + (imageHash == null ? \"\" : imageHash)\n + \"\\u0001\" + base64.length());\n return TranslationRequestQueue.submit(\n queueKey,\n SURFACE,\n TranslationRequestQueue.Priority.INTERACTIVE,\n 2,\n () -> sendOcrRequestBlocking(profile, prompt, base64))\n .thenApply(raw -> raw == null\n ? OcrResult.failure(\"OCR request failed after retries\")\n : parseOcrResult(raw));\n }\n\n @Override\n public CompletableFuture verifyAccess() {\n return translateImage(Base64.getDecoder().decode(OCR_PROBE_PNG_BASE64),\n \"ocr-vision-probe\", \"auto\", ModConfig.TARGET_LANGUAGE.get());\n }\n\n public CompletableFuture verifyAccess(boolean reuseTranslationModel,\n ModConfig.ApiFormat format,\n String apiUrl,\n String apiKey,\n String model) {\n ActiveProfile profile = reuseTranslationModel\n ? activeProfileFromConfig(true)\n : activeProfileFromValues(format, apiUrl, apiKey, model);\n if (!profile.ready()) {\n return CompletableFuture.completedFuture(OcrResult.failure(profile.errorMessage()));\n }\n String prompt = buildOcrPrompt(\"auto\", ModConfig.TARGET_LANGUAGE.get());\n String base64 = OCR_PROBE_PNG_BASE64;\n String queueKey = \"ocr-probe:\" + TranslationCacheKeys.hashSource(profile.queueIdentity());\n return TranslationRequestQueue.submit(\n queueKey,\n SURFACE + \".probe\",\n TranslationRequestQueue.Priority.INTERACTIVE,\n 1,\n () -> sendOcrRequestBlocking(profile, prompt, base64))\n .thenApply(raw -> isValidOcrProbeResponse(raw)\n ? parseOcrResult(raw)\n : OcrResult.failure(\"OCR model did not return the required JSON fields\"));\n }\n\n @Override\n public boolean isReady() {\n return activeProfile().ready();\n }\n\n @Override\n public String describeActiveProfile() {\n ActiveProfile profile = activeProfile();\n if (!profile.ready()) {\n return profile.errorMessage();\n }\n return profile.format().getDisplayName() + \" / \" + profile.model();\n }\n\n public static String buildOcrPrompt(String sourceLanguage, String targetLanguage) {\n String target = targetLanguage == null || targetLanguage.isBlank() ? \"zh_cn\" : targetLanguage;\n String source = sourceLanguage == null || sourceLanguage.isBlank() ? \"auto\" : sourceLanguage;\n return \"\"\"\n You are an OCR and translation engine for Minecraft screenshots.\n Read only visible text inside the image, in natural reading order.\n Ignore Minecraft world objects, cursor/crosshair, panels, borders, and non-text graphics.\n Source language: %s. Target language: %s.\n Return strict JSON only, with exactly:\n {\"sourceText\":\"all source text\",\"translationText\":\"all translated text\",\"regions\":[{\"sourceText\":\"one source block\",\"translationText\":\"its translation\",\"x\":0,\"y\":0,\"width\":1000,\"height\":1000}]}\n regions must contain one entry per visible text line. Do not merge separate lines or distant labels.\n x, y, width, and height are integers normalized to 0..1000 relative to the image.\n Each region must be the tight bounding rectangle of the source glyph pixels, not a loose panel,\n paragraph, translated-text estimate, or surrounding UI container.\n Keep regions in natural reading order. Every translation must correspond only to the source text\n inside that exact region.\n If the image contains no readable text, return {\"sourceText\":\"\",\"translationText\":\"\",\"regions\":[]}.\n Do not add markdown, comments, explanations, or extra fields.\n \"\"\".formatted(source, target);\n }\n\n public static JsonObject buildRequestBodyForTest(ModConfig.ApiFormat format, String model,\n String prompt, String base64Png) {\n EndpointKind kind = endpointKind(format);\n return buildRequestBody(kind, model == null || model.isBlank() ? format.getDefaultModel() : model,\n prompt == null ? \"\" : prompt, base64Png == null ? \"\" : base64Png);\n }\n\n public static OcrResult parseOcrResultForTest(String raw) {\n return parseOcrResult(raw);\n }\n\n private String sendOcrRequestBlocking(ActiveProfile profile, String prompt, String base64Png) {\n try {\n JsonObject body = buildRequestBody(profile.kind(), profile.model(), prompt, base64Png);\n HttpRequest.Builder builder = HttpRequest.newBuilder()\n .uri(URI.create(withApiKeyQuery(profile)))\n .timeout(Duration.ofSeconds(60))\n .header(\"Content-Type\", \"application/json\")\n .POST(HttpRequest.BodyPublishers.ofString(gson.toJson(body), StandardCharsets.UTF_8));\n applyAuthHeader(builder, profile);\n HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));\n if (response.statusCode() == 200) {\n if (looksLikeHtml(response.body())) {\n return failurePayload(\"OCR endpoint returned an HTML page; check API URL and interface format\");\n }\n String content = extractAssistantContent(response.body());\n return content == null || content.isBlank()\n ? failurePayload(\"OCR API returned an empty response\")\n : content;\n }\n String message = compactError(response.body());\n if (response.statusCode() == 429 || response.statusCode() >= 500) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n \"HTTP \" + response.statusCode() + (message.isBlank() ? \"\" : \": \" + message));\n }\n SimpleTranslateMod.getLogger().warn(\"OCR vision API error status={} message={}\",\n response.statusCode(), message);\n return failurePayload(\"HTTP \" + response.statusCode() + (message.isBlank() ? \"\" : \": \" + message));\n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n return failurePayload(\"OCR request interrupted\");\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"OCR I/O error\" : e.getMessage(), e);\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n throw e;\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().warn(\"OCR vision request failed: {}\",\n e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());\n return failurePayload(e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage());\n }\n }\n\n private static JsonObject buildRequestBody(EndpointKind kind, String model, String prompt, String base64Png) {\n return switch (kind) {\n case CHAT_COMPLETIONS -> buildChatCompletionsBody(model, prompt, base64Png);\n case RESPONSES -> buildResponsesBody(model, prompt, base64Png);\n case ANTHROPIC_MESSAGES -> buildAnthropicMessagesBody(model, prompt, base64Png);\n case GEMINI_GENERATE_CONTENT -> buildGeminiGenerateContentBody(prompt, base64Png);\n };\n }\n\n private static JsonObject buildChatCompletionsBody(String model, String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n root.addProperty(\"model\", model);\n root.addProperty(\"temperature\", 0);\n root.addProperty(\"max_tokens\", MAX_OCR_OUTPUT_TOKENS);\n\n JsonArray messages = new JsonArray();\n JsonObject user = new JsonObject();\n user.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject text = new JsonObject();\n text.addProperty(\"type\", \"text\");\n text.addProperty(\"text\", prompt);\n content.add(text);\n JsonObject image = new JsonObject();\n image.addProperty(\"type\", \"image_url\");\n JsonObject imageUrl = new JsonObject();\n imageUrl.addProperty(\"url\", dataUrl(base64Png));\n imageUrl.addProperty(\"detail\", \"high\");\n image.add(\"image_url\", imageUrl);\n content.add(image);\n user.add(\"content\", content);\n messages.add(user);\n root.add(\"messages\", messages);\n return root;\n }\n\n private static JsonObject buildResponsesBody(String model, String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n root.addProperty(\"model\", model);\n root.addProperty(\"max_output_tokens\", MAX_OCR_OUTPUT_TOKENS);\n\n JsonArray input = new JsonArray();\n JsonObject user = new JsonObject();\n user.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject text = new JsonObject();\n text.addProperty(\"type\", \"input_text\");\n text.addProperty(\"text\", prompt);\n content.add(text);\n JsonObject image = new JsonObject();\n image.addProperty(\"type\", \"input_image\");\n image.addProperty(\"image_url\", dataUrl(base64Png));\n image.addProperty(\"detail\", \"high\");\n content.add(image);\n user.add(\"content\", content);\n input.add(user);\n root.add(\"input\", input);\n return root;\n }\n\n private static JsonObject buildAnthropicMessagesBody(String model, String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n root.addProperty(\"model\", model);\n root.addProperty(\"max_tokens\", MAX_OCR_OUTPUT_TOKENS);\n root.addProperty(\"temperature\", 0);\n root.addProperty(\"system\", \"Return strict JSON only.\");\n\n JsonArray messages = new JsonArray();\n JsonObject user = new JsonObject();\n user.addProperty(\"role\", \"user\");\n JsonArray content = new JsonArray();\n JsonObject image = new JsonObject();\n image.addProperty(\"type\", \"image\");\n JsonObject source = new JsonObject();\n source.addProperty(\"type\", \"base64\");\n source.addProperty(\"media_type\", \"image/png\");\n source.addProperty(\"data\", base64Png);\n image.add(\"source\", source);\n content.add(image);\n JsonObject text = new JsonObject();\n text.addProperty(\"type\", \"text\");\n text.addProperty(\"text\", prompt);\n content.add(text);\n user.add(\"content\", content);\n messages.add(user);\n root.add(\"messages\", messages);\n return root;\n }\n\n private static JsonObject buildGeminiGenerateContentBody(String prompt, String base64Png) {\n JsonObject root = new JsonObject();\n JsonArray contents = new JsonArray();\n JsonObject content = new JsonObject();\n JsonArray parts = new JsonArray();\n JsonObject image = new JsonObject();\n JsonObject inlineData = new JsonObject();\n inlineData.addProperty(\"mimeType\", \"image/png\");\n inlineData.addProperty(\"data\", base64Png);\n image.add(\"inlineData\", inlineData);\n parts.add(image);\n JsonObject text = new JsonObject();\n text.addProperty(\"text\", prompt);\n parts.add(text);\n content.add(\"parts\", parts);\n contents.add(content);\n root.add(\"contents\", contents);\n JsonObject generationConfig = new JsonObject();\n generationConfig.addProperty(\"temperature\", 0);\n generationConfig.addProperty(\"maxOutputTokens\", MAX_OCR_OUTPUT_TOKENS);\n root.add(\"generationConfig\", generationConfig);\n return root;\n }\n\n private ActiveProfile activeProfile() {\n boolean reuse = ModConfig.OCR_USE_TRANSLATION_MODEL.get();\n if (reuse) {\n ActiveProfile translationProfile = activeProfileFromConfig(true);\n if (translationProfile.ready()) {\n return translationProfile;\n }\n ActiveProfile dedicatedProfile = activeProfileFromConfig(false);\n if (dedicatedProfile.ready()) {\n return dedicatedProfile;\n }\n return ActiveProfile.error(translationProfile.format(),\n translationProfile.errorMessage() + \"; OCR dedicated model: \" + dedicatedProfile.errorMessage());\n }\n return activeProfileFromConfig(false);\n }\n\n private ActiveProfile activeProfileFromConfig(boolean reuse) {\n ModConfig.ApiFormat format = reuse ? ModConfig.API_FORMAT.get() : ModConfig.OCR_API_FORMAT.get();\n String url = reuse ? ModConfig.DEEPSEEK_API_URL.get() : ModConfig.OCR_API_URL.get();\n String key = reuse ? ModConfig.DEEPSEEK_API_KEY.get() : ModConfig.OCR_API_KEY.get();\n String model = reuse ? ModConfig.DEEPSEEK_MODEL.get() : ModConfig.OCR_MODEL.get();\n return activeProfileFromValues(format, url, key, model);\n }\n\n private ActiveProfile activeProfileFromValues(ModConfig.ApiFormat format, String apiUrl,\n String apiKey, String configuredModel) {\n if (!isVisionFormat(format)) {\n return ActiveProfile.error(format, \"OCR model format does not support image input\");\n }\n String key = sanitizeApiKey(apiKey);\n if (key.isBlank()) {\n return ActiveProfile.error(format, \"OCR API key not configured\");\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return ActiveProfile.error(format, urlError);\n }\n String url = ModConfig.normalizeApiUrl(apiUrl);\n String model = format == ModConfig.ApiFormat.DEEPSEEK_CHAT\n ? normalizedTranslationModel(format, configuredModel)\n : normalizedOcrModel(format, configuredModel);\n if (model.isBlank()) {\n return ActiveProfile.error(format, \"OCR model not configured\");\n }\n EndpointKind kind = endpointKind(format);\n String endpoint = resolveEndpoint(url, kind, model);\n return new ActiveProfile(true, \"\", format, kind, url, endpoint, key, model);\n }\n\n private static boolean isValidOcrProbeResponse(String raw) {\n if (raw == null || raw.isBlank() || raw.startsWith(OCR_ERROR_PREFIX)) {\n return false;\n }\n String json = objectSlice(stripFences(ModelOutputSanitizer.sanitize(raw)));\n if (json.isBlank()) {\n return false;\n }\n try {\n JsonObject object = JsonParser.parseString(json).getAsJsonObject();\n return object.has(\"sourceText\") && object.has(\"translationText\");\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(Locale.ROOT);\n return trimmed.startsWith(\" resolveChatCompletionsEndpoint(normalized);\n case RESPONSES -> resolveResponsesEndpoint(normalized);\n case ANTHROPIC_MESSAGES -> resolveAnthropicMessagesEndpoint(normalized);\n case GEMINI_GENERATE_CONTENT -> resolveGeminiGenerateContentEndpoint(normalized, model);\n };\n }\n\n private static String resolveChatCompletionsEndpoint(String normalized) {\n if (normalized.contains(\"/chat/completions\")) {\n return normalized;\n }\n if (normalized.endsWith(\"/v1\") || normalized.endsWith(\"/v1/\")) {\n return normalized.replaceAll(\"/+$\", \"\") + \"/chat/completions\";\n }\n return normalized + \"/chat/completions\";\n }\n\n private static String resolveResponsesEndpoint(String normalized) {\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 static String resolveAnthropicMessagesEndpoint(String normalized) {\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 static String resolveGeminiGenerateContentEndpoint(String normalized, String model) {\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 void applyAuthHeader(HttpRequest.Builder builder, ActiveProfile profile) {\n if (profile.kind() == EndpointKind.ANTHROPIC_MESSAGES) {\n builder.header(\"x-api-key\", profile.apiKey());\n builder.header(\"anthropic-version\", \"2023-06-01\");\n } else if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT) {\n builder.header(\"Authorization\", \"Bearer \" + profile.apiKey());\n }\n }\n\n private String withApiKeyQuery(ActiveProfile profile) {\n if (profile.kind() != EndpointKind.GEMINI_GENERATE_CONTENT || profile.endpointUrl().contains(\"key=\")) {\n return profile.endpointUrl();\n }\n return profile.endpointUrl() + (profile.endpointUrl().contains(\"?\") ? \"&\" : \"?\")\n + \"key=\" + URLEncoder.encode(profile.apiKey(), StandardCharsets.UTF_8);\n }\n\n private static String extractAssistantContent(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return \"\";\n }\n try {\n JsonObject response = JsonParser.parseString(responseBody).getAsJsonObject();\n String extracted = extractAssistantContent(response);\n return extracted == null ? \"\" : extracted;\n } catch (Exception ignored) {\n return responseBody;\n }\n }\n\n private static String extractAssistantContent(JsonObject response) {\n if (response == null) {\n return \"\";\n }\n if (response.has(\"sourceText\") || response.has(\"translationText\")) {\n return response.toString();\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 String content = textFromJsonContent(message.get(\"content\"));\n if (!content.isBlank()) {\n return content;\n }\n }\n }\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 String text = textFromOutputArray(response.getAsJsonArray(\"output\"));\n if (!text.isBlank()) {\n return text;\n }\n }\n if (response.has(\"content\") && response.get(\"content\").isJsonArray()) {\n String text = textFromJsonContent(response.get(\"content\"));\n if (!text.isBlank()) {\n return text;\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\")) {\n return textFromJsonContent(content.get(\"parts\"));\n }\n }\n }\n }\n return \"\";\n }\n\n private static String textFromOutputArray(JsonArray output) {\n StringBuilder builder = new StringBuilder();\n for (JsonElement element : output) {\n if (!element.isJsonObject()) {\n continue;\n }\n JsonObject object = element.getAsJsonObject();\n if (object.has(\"content\")) {\n String content = textFromJsonContent(object.get(\"content\"));\n if (!content.isBlank()) {\n if (!builder.isEmpty()) {\n builder.append('\\n');\n }\n builder.append(content);\n }\n }\n }\n return builder.toString();\n }\n\n private static String textFromJsonContent(JsonElement element) {\n if (element == null || element.isJsonNull()) {\n return \"\";\n }\n if (element.isJsonPrimitive()) {\n return element.getAsString();\n }\n if (!element.isJsonArray()) {\n return \"\";\n }\n StringBuilder builder = new StringBuilder();\n for (JsonElement item : element.getAsJsonArray()) {\n if (!item.isJsonObject()) {\n continue;\n }\n JsonObject part = item.getAsJsonObject();\n if (part.has(\"text\") && !part.get(\"text\").isJsonNull()) {\n if (!builder.isEmpty()) {\n builder.append('\\n');\n }\n builder.append(part.get(\"text\").getAsString());\n }\n }\n return builder.toString();\n }\n\n private static OcrResult parseOcrResult(String raw) {\n if (raw != null && raw.startsWith(OCR_ERROR_PREFIX)) {\n return OcrResult.failure(raw.substring(OCR_ERROR_PREFIX.length()).trim());\n }\n String cleaned = stripFences(ModelOutputSanitizer.sanitize(raw));\n if (cleaned == null || cleaned.isBlank()) {\n return OcrResult.success(\"\", \"\");\n }\n String json = objectSlice(cleaned);\n if (!json.isBlank()) {\n try {\n JsonObject object = JsonParser.parseString(json).getAsJsonObject();\n String source = stringField(object, \"sourceText\");\n String translation = stringField(object, \"translationText\");\n List regions = parseRegions(object.get(\"regions\"));\n return OcrResult.success(source, translation, regions);\n } catch (Exception ignored) {\n // Fall through to non-JSON recovery.\n }\n }\n return OcrResult.success(\"\", cleaned);\n }\n\n private static String stripFences(String value) {\n if (value == null) {\n return \"\";\n }\n String text = value.trim();\n if (text.startsWith(\"```\")) {\n text = text.replaceFirst(\"(?is)^```(?:json|text)?\\\\s*\", \"\");\n text = text.replaceFirst(\"(?is)\\\\s*```$\", \"\");\n }\n return text.trim();\n }\n\n private static String objectSlice(String value) {\n int start = value == null ? -1 : value.indexOf('{');\n int end = value == null ? -1 : value.lastIndexOf('}');\n if (start < 0 || end <= start) {\n return \"\";\n }\n return value.substring(start, end + 1);\n }\n\n private static String stringField(JsonObject object, String key) {\n if (object == null || !object.has(key) || object.get(key).isJsonNull()) {\n return \"\";\n }\n try {\n return object.get(key).getAsString();\n } catch (Exception ignored) {\n return \"\";\n }\n }\n\n private static List parseRegions(JsonElement element) {\n if (element == null || !element.isJsonArray()) {\n return List.of();\n }\n List regions = new ArrayList<>();\n for (JsonElement item : element.getAsJsonArray()) {\n if (!item.isJsonObject()) {\n continue;\n }\n JsonObject object = item.getAsJsonObject();\n String source = firstStringField(object, \"sourceText\", \"source\", \"text\");\n String translation = firstStringField(object, \"translationText\", \"translation\", \"translatedText\");\n JsonArray box = firstArrayField(object, \"bbox\", \"box\", \"bounds\");\n int x = coordinate(object, box, 0, \"x\", \"left\");\n int y = coordinate(object, box, 1, \"y\", \"top\");\n int width = coordinate(object, box, 2, \"width\", \"w\");\n int height = coordinate(object, box, 3, \"height\", \"h\");\n if (width <= 0 || height <= 0 || translation.isBlank()) {\n continue;\n }\n regions.add(new OcrRegion(source, translation, x, y, width, height).sanitized());\n }\n return regions;\n }\n\n private static String firstStringField(JsonObject object, String... keys) {\n for (String key : keys) {\n String value = stringField(object, key);\n if (!value.isBlank()) {\n return value;\n }\n }\n return \"\";\n }\n\n private static JsonArray firstArrayField(JsonObject object, String... keys) {\n for (String key : keys) {\n if (object.has(key) && object.get(key).isJsonArray()) {\n return object.getAsJsonArray(key);\n }\n }\n return null;\n }\n\n private static int coordinate(JsonObject object, JsonArray box, int boxIndex, String... keys) {\n JsonElement value = null;\n for (String key : keys) {\n if (object.has(key) && !object.get(key).isJsonNull()) {\n value = object.get(key);\n break;\n }\n }\n if (value == null && box != null && box.size() > boxIndex) {\n value = box.get(boxIndex);\n }\n if (value == null || value.isJsonNull()) {\n return 0;\n }\n try {\n double number = value.getAsDouble();\n if (number > 0.0 && number <= 1.0) {\n number *= 1000.0;\n }\n return (int) Math.round(number);\n } catch (Exception ignored) {\n return 0;\n }\n }\n\n private static String sanitizeApiKey(String apiKey) {\n if (apiKey == null) {\n return \"\";\n }\n return apiKey.trim();\n }\n\n private static String dataUrl(String base64Png) {\n return \"data:image/png;base64,\" + (base64Png == null ? \"\" : base64Png);\n }\n\n private static String failurePayload(String message) {\n String compact = message == null || message.isBlank() ? \"OCR request failed\" : message.trim();\n compact = compact.replace('\\n', ' ').replace('\\r', ' ');\n return OCR_ERROR_PREFIX + (compact.length() > 240 ? compact.substring(0, 237) + \"...\" : compact);\n }\n\n private static String compactError(String body) {\n if (body == null || body.isBlank()) {\n return \"\";\n }\n String compact = body.replace('\\n', ' ').replace('\\r', ' ').trim();\n return compact.length() > 240 ? compact.substring(0, 237) + \"...\" : compact;\n }\n\n private record ActiveProfile(\n boolean ready,\n String errorMessage,\n ModConfig.ApiFormat format,\n EndpointKind kind,\n String endpointBase,\n String endpointUrl,\n String apiKey,\n String model) {\n static ActiveProfile error(ModConfig.ApiFormat format, String message) {\n return new ActiveProfile(false, message == null ? \"OCR not configured\" : message,\n format == null ? ModConfig.ApiFormat.OPENAI_RESPONSES : format,\n EndpointKind.RESPONSES, \"\", \"\", \"\", \"\");\n }\n\n String queueIdentity() {\n return (format == null ? \"\" : format.name().toLowerCase(Locale.ROOT))\n + \"|\" + endpointBase + \"|\" + endpointUrl + \"|\" + model;\n }\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067441.7643867, "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"}}, "fabric/SimpleTranslate-Fabric-1.21.9/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067442.1904533, "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"}}, "neoforge/SimpleTranslate-Forge-1.19.2/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067442.2169168, "n": 11178, "h": "5c77ff3a382ab6c0", "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 UiCompat.setHint(this.apiUrlInput, 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 UiCompat.setHint(this.apiKeyInput, 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 UiCompat.setHint(this.modelInput, 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 = UiCompat.buttonBuilder(\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/config/ModConfig.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067442.601329, "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"}}, "neoforge/SimpleTranslate-NeoForge-1.20.6/src/main/java/com/yourname/simpletranslate/translation/DeepSeekTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067442.7604916, "n": 60896, "h": "4db9c81e047177fe", "c": "package com.yourname.simpletranslate.translation;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.util.ModelOutputSanitizer;\nimport com.yourname.simpletranslate.util.TranslationCacheKeys;\nimport com.yourname.simpletranslate.util.TranslationMapping;\nimport com.yourname.simpletranslate.util.TranslationMappingResult;\nimport com.yourname.simpletranslate.util.TranslationSlot;\nimport com.yourname.simpletranslate.util.TranslationTextDetector;\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.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 private static final Pattern ALLOWED_MAPPING_PLACEHOLDER_PATTERN = Pattern.compile(\"@@@F\\\\d+@@@\");\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 = 8192;\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 DIRECT_SURFACE_PATTERN = Pattern.compile(\"]*\\\\bsurface=\\\"([^\\\"]+)\\\"\");\n private static final Pattern MAPPING_SURFACE_PATTERN = Pattern.compile(\"\\\"surface\\\"\\\\s*:\\\\s*\\\"([^\\\"]+)\\\"\");\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 /** Alias so existing references keep working after the prompt-builder split. */\n private static final class RequestMode {\n static final TranslationPrompts.RequestMode PLAIN_TEXT = TranslationPrompts.RequestMode.PLAIN_TEXT;\n static final TranslationPrompts.RequestMode PRESERVE_MARKERS = TranslationPrompts.RequestMode.PRESERVE_MARKERS;\n static final TranslationPrompts.RequestMode MAPPING_JSON = TranslationPrompts.RequestMode.MAPPING_JSON;\n static final TranslationPrompts.RequestMode DIRECT_FORMATTED = TranslationPrompts.RequestMode.DIRECT_FORMATTED;\n\n private RequestMode() {\n }\n }\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 @Override\n public CompletableFuture translate(String text, String sourceLanguage, String targetLanguage) {\n return translateWithTerms(text, sourceLanguage, targetLanguage, List.of());\n }\n\n @Override\n public CompletableFuture translateWithTerms(String text, String sourceLanguage, String targetLanguage,\n List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.PLAIN_TEXT);\n return sendRequest(systemPrompt, text);\n }\n\n @Override\n public CompletableFuture translatePreservingMarkers(String text, String sourceLanguage,\n String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.PRESERVE_MARKERS);\n return sendRequest(systemPrompt, text);\n }\n\n @Override\n public CompletableFuture translateFormattedDocument(String document, String surface,\n String sourceLanguage, String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n // Style/term hints live in the user payload so the system prompt stays\n // byte-stable per language pair (provider prefix caching).\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, List.of(),\n RequestMode.DIRECT_FORMATTED);\n String userPrompt = insertDirectSections(document, TranslationPrompts.directUserSections(termHints));\n return sendRequest(systemPrompt, userPrompt, estimateDirectMaxTokens(document), surface);\n }\n\n /** Same as {@link #translateFormattedDocument} but scales the output token budget. */\n public CompletableFuture translateFormattedDocument(String document, String surface,\n String sourceLanguage, String targetLanguage, List termHints, int maxTokenMultiplier) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, List.of(),\n RequestMode.DIRECT_FORMATTED);\n String userPrompt = insertDirectSections(document, TranslationPrompts.directUserSections(termHints));\n return sendRequest(systemPrompt, userPrompt, estimateDirectMaxTokens(document, maxTokenMultiplier), surface);\n }\n\n /** Inserts [STYLE]/[TERMS] sections right before the [TEXT] block. */\n private static String insertDirectSections(String document, String sections) {\n if (sections == null || sections.isBlank() || document == null) {\n return document;\n }\n int index = document.indexOf(\"[TEXT\");\n if (index < 0) {\n return sections + document;\n }\n return document.substring(0, index) + sections + document.substring(index);\n }\n\n /** Output budget follows the [TEXT] block size, not the whole payload with context. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Output budget follows the [TEXT] block; multiplier supports partial retries. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int start = body.indexOf(\"[TEXT\");\n int end = body.lastIndexOf(\"[/TEXT]\");\n if (start >= 0 && end > start) {\n body = body.substring(start, end);\n }\n int lineCount = 0;\n for (String line : body.split(\"\\\\R\")) {\n if (line != null && line.matches(\"(?s).*?\\\\d{1,4}\\\\s*[||].*\")) {\n lineCount++;\n }\n }\n // Chinese game text often expands; multi-line tooltips need headroom per line.\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int lineEstimate = lineCount * 192 + 384;\n int estimated = Math.max(charEstimate, lineEstimate);\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 @Override\n public CompletableFuture translateMapping(TranslationMapping mapping,\n String sourceLanguage, String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.MAPPING_JSON);\n String userPrompt = buildMappingUserPrompt(mapping, sourceLanguage, targetLanguage);\n return sendRequest(systemPrompt, userPrompt).thenApply(raw -> parseMappingResponse(raw, mapping));\n }\n\n @Override\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationService.ApiDetectionResult(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 @Override\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationService.ModelDetectionResult(\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 @Override\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 TranslationService.ModelAccessResult(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationService.ModelAccessResult(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationService.ModelAccessResult(\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 String buildSystemPrompt(String sourceLanguage, String targetLanguage, List termHints,\n TranslationPrompts.RequestMode mode) {\n return TranslationPrompts.buildSystemPrompt(sourceLanguage, targetLanguage, termHints, mode);\n }\n\n\n private String buildMappingUserPrompt(TranslationMapping mapping, String sourceLanguage, String targetLanguage) {\n JsonObject root = new JsonObject();\n root.addProperty(\"version\", \"mapping-v1\");\n root.addProperty(\"jobId\", mapping.taskId());\n root.addProperty(\"surface\", mapping.surface());\n root.addProperty(\"sourceLanguage\", getLanguageName(sourceLanguage));\n root.addProperty(\"targetLanguage\", getLanguageName(targetLanguage));\n root.addProperty(\"context\", mapping.context());\n root.addProperty(\"layoutSignature\", mapping.layoutSignature());\n root.addProperty(\"styleSignature\", mapping.styleSignature());\n boolean flexibleLayout = isFlexibleMappingSurface(mapping.surface());\n root.addProperty(\"layoutMode\", flexibleLayout ? \"flexible-tooltip\" : \"fixed-layout\");\n root.addProperty(\"instructions\", flexibleLayout\n ? \"Translate every unit as its own semantic style block or styled token. Use natural Minecraft localization for the target language, ignore source visual line breaks, keep every @@@F marker exactly once in the same unit, and preserve only structural tokens such as numbers, ids, commands, control codes, and placeholders.\"\n : \"Translate each fixed-layout slot conservatively. Preserve line/slot structure, technical tokens, and every @@@F marker exactly.\");\n\n JsonArray units = new JsonArray();\n for (TranslationSlot slot : mapping.slots()) {\n JsonObject unit = new JsonObject();\n unit.addProperty(\"id\", slot.id());\n unit.addProperty(\"text\", slot.text());\n unit.addProperty(\"role\", slot.role());\n unit.addProperty(\"maxChars\", slot.maxWidth());\n unit.addProperty(\"fixed\", slot.fixedSlot());\n unit.addProperty(\"lineIndex\", slot.lineIndex());\n unit.addProperty(\"slotIndex\", slot.slotIndex());\n unit.addProperty(\"styleSignature\", slot.styleSignature());\n unit.addProperty(\"tokenMask\", slot.tokenMask());\n units.add(unit);\n }\n root.add(\"units\", units);\n return root.toString();\n }\n\n private boolean isFlexibleMappingSurface(String surface) {\n if (surface == null) {\n return false;\n }\n String normalized = surface.toLowerCase();\n return normalized.startsWith(\"tooltip.\")\n || normalized.startsWith(\"hover.\")\n || normalized.startsWith(\"book.\");\n }\n\n private String getLanguageName(String code) {\n return TranslationTextDetector.displayLanguageName(code);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt) {\n return sendRequest(systemPrompt, userPrompt, estimateMaxTokens(userPrompt));\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens) {\n return sendRequest(systemPrompt, userPrompt, maxTokens, null);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String explicitSurface) {\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 surface = explicitSurface == null || explicitSurface.isBlank()\n ? detectRequestSurface(userPrompt)\n : explicitSurface;\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestBlocking(profiles, apiKey, model, systemPrompt, userPrompt, maxTokens));\n }\n\n private String sendRequestBlocking(List profiles, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n for (ApiRequestProfile profile : orderedProfiles) {\n String translated = sendRequestWithProfile(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return translated;\n }\n }\n return null;\n }\n\n private String sendRequestWithProfile(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return null;\n }\n\n long startedAt = System.nanoTime();\n try {\n HttpRequest request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n String result = parseResponse(response.body(), startedAt);\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 return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\"Empty translation result from DeepSeek\");\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 (InterruptedException e) {\n Thread.currentThread().interrupt();\n SimpleTranslateMod.getLogger().error(\"Translation request interrupted\", e);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n throw e;\n } catch (InvalidApiResponseException e) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\", e.getMessage());\n return null;\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\", e.getMessage(), e);\n return null;\n }\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 String detectRequestSurface(String userPrompt) {\n if (userPrompt == null || userPrompt.isBlank()) {\n return \"manager.raw\";\n }\n Matcher direct = DIRECT_SURFACE_PATTERN.matcher(userPrompt);\n if (direct.find()) {\n return direct.group(1);\n }\n Matcher mapping = MAPPING_SURFACE_PATTERN.matcher(userPrompt);\n if (mapping.find()) {\n return mapping.group(1);\n }\n return \"manager.raw\";\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 static int estimateMaxTokens(String text) {\n int length = text == null ? 0 : text.length();\n int estimated = length + 512;\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n return MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n return Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.contains(\"data:\")) {\n return parseStreamingResponse(responseBody);\n }\n return parsePlainResponse(responseBody);\n }\n\n private String parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return null;\n }\n\n Future readerTask = 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 null;\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 String parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return null;\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\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 null;\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);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return null;\n }\n\n if (sawData) {\n return ModelOutputSanitizer.sanitize(result.toString());\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 String parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\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);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return null;\n }\n return ModelOutputSanitizer.sanitize(result.toString());\n }\n\n private String parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\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 ModelOutputSanitizer.sanitize(raw);\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 TranslationMappingResult parseMappingResponse(String response, TranslationMapping mapping) {\n String sanitized = ModelOutputSanitizer.sanitize(response);\n if (sanitized == null || sanitized.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n try {\n String jsonPayload = extractJsonObjectPayload(sanitized);\n if (jsonPayload == null || jsonPayload.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n JsonObject root = JsonParser.parseString(jsonPayload).getAsJsonObject();\n String version = root.has(\"version\") ? root.get(\"version\").getAsString() : \"\";\n String jobId = root.has(\"jobId\") ? root.get(\"jobId\").getAsString() : \"\";\n if (!\"mapping-v1\".equals(version) || !mapping.taskId().equals(jobId)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n if (!root.has(\"translations\") || !root.get(\"translations\").isJsonArray()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n Map translations = new LinkedHashMap<>();\n for (var element : root.getAsJsonArray(\"translations\")) {\n if (!element.isJsonObject()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n JsonObject item = element.getAsJsonObject();\n String id = item.has(\"id\") ? item.get(\"id\").getAsString() : \"\";\n String translation = item.has(\"translation\") ? item.get(\"translation\").getAsString() : \"\";\n if (id.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n String cleaned = ModelOutputSanitizer.sanitize(translation);\n if (cleaned == null || cleaned.isBlank() || containsDisallowedMappingTokens(cleaned)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n translations.put(id, cleaned);\n }\n\n if (translations.size() != mapping.slots().size()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n for (TranslationSlot slot : mapping.slots()) {\n if (!translations.containsKey(slot.id())) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n if (slot.fixedSlot()) {\n String normalizedSource = TranslationCacheKeys.normalizeSource(slot.text());\n String normalizedTranslated = TranslationCacheKeys.normalizeSource(translations.get(slot.id()));\n if (!normalizedSource.equals(normalizedTranslated)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n }\n }\n\n return new TranslationMappingResult(mapping.taskId(), translations);\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().debug(\"Failed to parse mapping response\", e);\n return new TranslationMappingResult(mapping.taskId(), Map.of());\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 boolean containsDisallowedMappingTokens(String text) {\n if (text == null || text.isEmpty()) {\n return false;\n }\n return containsDisallowedMappingMarker(text)\n || text.contains(\"```\")\n || text.contains(\"\")\n || text.contains(\"\")\n || text.contains(\"\")\n || text.contains(\"\");\n }\n\n private boolean containsDisallowedMappingMarker(String text) {\n int index = text.indexOf(\"@@@\");\n if (index < 0) {\n return false;\n }\n Matcher matcher = ALLOWED_MAPPING_PLACEHOLDER_PATTERN.matcher(text);\n int cursor = 0;\n while (matcher.find()) {\n String between = text.substring(cursor, matcher.start());\n if (between.contains(\"@@@\")) {\n return true;\n }\n cursor = matcher.end();\n }\n return text.substring(cursor).contains(\"@@@\");\n }\n\n private void appendStreamingChunk(StringBuilder result, String json) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\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 TranslationService.ApiDetectionResult 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 = sendRequestWithProfile(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationService.ApiDetectionResult(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 TranslationService.ApiDetectionResult(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationService.ModelAccessResult 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 = buildSystemPrompt(\"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(),\n RequestMode.DIRECT_FORMATTED);\n String probeUserPrompt = \"\"\"\n \n [TEXT lines=1]\n 0|Hello\n [/TEXT]\n \n \"\"\";\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.send(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream());\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 TranslationService.ModelAccessResult(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 (InterruptedException e) {\n Thread.currentThread().interrupt();\n return new TranslationService.ModelAccessResult(false, modelId, 0, \"Model verification interrupted\");\n } catch (IOException | RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationService.ModelAccessResult(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n for (String line : value.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (trimmed.matches(\"0\\\\s*[||].+\")) {\n return true;\n }\n }\n return false;\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, false));\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 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(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 requestBody.addProperty(\"max_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n if (!thinkingEnabled) {\n requestBody.addProperty(\"temperature\", 0.1);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\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 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 requestBody.addProperty(\"max_output_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n requestBody.addProperty(\"temperature\", 0.1);\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 requestBody.addProperty(\"max_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n requestBody.addProperty(\"temperature\", 0.1);\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 systemPrompt, String userPrompt, 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.1);\n generationConfig.addProperty(\"maxOutputTokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\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 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 @Override\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n @Override\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.11/src/main/java/com/yourname/simpletranslate/translation/DeepSeekTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067442.818539, "n": 66162, "h": "c0e03c9eab7063da", "c": "package com.yourname.simpletranslate.translation;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.util.ModelOutputSanitizer;\nimport com.yourname.simpletranslate.util.TranslationCacheKeys;\nimport com.yourname.simpletranslate.util.TranslationMapping;\nimport com.yourname.simpletranslate.util.TranslationMappingResult;\nimport com.yourname.simpletranslate.util.TranslationSlot;\nimport com.yourname.simpletranslate.util.TranslationTextDetector;\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.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 private static final Pattern ALLOWED_MAPPING_PLACEHOLDER_PATTERN = Pattern.compile(\"@@@F\\\\d+@@@\");\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 = 8192;\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 Pattern DIRECT_SURFACE_PATTERN = Pattern.compile(\"]*\\\\bsurface=\\\"([^\\\"]+)\\\"\");\n private static final Pattern MAPPING_SURFACE_PATTERN = Pattern.compile(\"\\\"surface\\\"\\\\s*:\\\\s*\\\"([^\\\"]+)\\\"\");\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 RequestMode {\n PLAIN_TEXT,\n PRESERVE_MARKERS,\n MAPPING_JSON,\n DIRECT_FORMATTED\n }\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 @Override\n public CompletableFuture translate(String text, String sourceLanguage, String targetLanguage) {\n return translateWithTerms(text, sourceLanguage, targetLanguage, List.of());\n }\n\n @Override\n public CompletableFuture translateWithTerms(String text, String sourceLanguage, String targetLanguage,\n List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.PLAIN_TEXT);\n return sendRequest(systemPrompt, text);\n }\n\n @Override\n public CompletableFuture translatePreservingMarkers(String text, String sourceLanguage,\n String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.PRESERVE_MARKERS);\n return sendRequest(systemPrompt, text);\n }\n\n @Override\n public CompletableFuture translateFormattedDocument(String document, String sourceLanguage,\n String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.DIRECT_FORMATTED);\n return sendRequest(systemPrompt, document, estimateMaxTokens(document));\n }\n\n @Override\n public CompletableFuture translateMapping(TranslationMapping mapping,\n String sourceLanguage, String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.MAPPING_JSON);\n String userPrompt = buildMappingUserPrompt(mapping, sourceLanguage, targetLanguage);\n return sendRequest(systemPrompt, userPrompt).thenApply(raw -> parseMappingResponse(raw, mapping));\n }\n\n @Override\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationService.ApiDetectionResult(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 @Override\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationService.ModelDetectionResult(\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 @Override\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 TranslationService.ModelAccessResult(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationService.ModelAccessResult(\n false, model, 0, \"Model ID not configured\"));\n }\n String endpoint = ModConfig.normalizeApiUrl(apiUrl);\n return CompletableFuture.supplyAsync(() -> probeModelAccess(endpoint, key, model, format), DETECTION_EXECUTOR);\n }\n\n private String buildSystemPrompt(String sourceLanguage, String targetLanguage, List termHints,\n RequestMode mode) {\n StringBuilder prompt = new StringBuilder();\n prompt.append(\"You are a professional translator for Minecraft game content. \");\n String sourceCode = TranslationTextDetector.canonicalLanguageCode(sourceLanguage);\n if (\"auto\".equals(sourceCode)) {\n prompt.append(\"Auto-detect the source language and translate the following text to \");\n } else {\n prompt.append(\"Translate the following text from \").append(getLanguageName(sourceLanguage)).append(\" to \");\n }\n prompt.append(getLanguageName(targetLanguage)).append(\". \");\n prompt.append(\"Minecraft formatting, colors, styles, and events are restored by the client code. \");\n prompt.append(\"Only output the translated text without any explanation or additional content. \");\n prompt.append(\"Do not echo the source text or produce bilingual output unless the source itself must stay unchanged. \");\n prompt.append(\"For Minecraft UI and item tooltip text, translate every visible natural-language word or phrase into the target language; do not append, repeat, or keep the source beside the translation. \");\n prompt.append(\"Do not leave UI labels such as Abbreviations, Hover on their names, Objectives, Enemies, Mana, Damage, Ability, Used, or fullwidth Latin status text unchanged. \");\n prompt.append(\"Keep only player names, namespace IDs, commands, formatting codes, numbers, placeholders, and symbols unchanged when they are not natural-language text. \");\n prompt.append(\"Use fluent game-localization wording in the target language. Mechanics should be short, precise, and natural; lore should read naturally. \");\n prompt.append(\"Stay faithful to the source facts: do not invent lore, targets, triggers, ranges, or effects. \");\n prompt.append(\"Keep important item type nouns explicit in names: translate Charm and Amulet as 护符, Staff as 法杖, Blueprint as 蓝图, Ballista as 弩炮, Guardian/Warden as 守卫, and do not compress them away. \");\n prompt.append(\"Translate natural-language text inside existing brackets while preserving the bracket characters and count, for example [Root] -> [缠绕], [Burn] -> [燃烧], [Phase] -> [相位], [Click to TP] -> [点击传送], and [Right-Click] -> [右键]. \");\n prompt.append(\"In tooltip mechanics, a leading phrase like 'For 10s,' is a duration ('持续 10s'), not a range. Phrases like 'every 0.8s' are intervals. \");\n prompt.append(\"In game status text, 'without [Status]' means the target does not currently have that status; never translate it as 'will not trigger [Status]'. \");\n prompt.append(\"Do not add quotation marks, brackets, punctuation, or explanatory wording unless they already exist in the source text or are represented by a frozen placeholder. \");\n prompt.append(\"If the text contains player names or coordinates, keep them unchanged. \");\n if (mode == RequestMode.PRESERVE_MARKERS) {\n prompt.append(\"Never modify control markers/tokens such as §§§, @@@F@@@, @@@CTX@@@, @@@TTS@@@, @@@S@@@, @@@SL@@@, @@@S_END@@@, \");\n prompt.append(\"§§§PAGE§§§, §§§TITLE§§§, or bracketed marker tags. \");\n prompt.append(\"@@@F@@@ markers are frozen style placeholders; keep every one exactly. \");\n prompt.append(\"Keep marker order/count and line breaks unchanged. \");\n prompt.append(\"When @@@TTS@@@ markers are present, preserve every marker exactly, translate only the text after each marker until the next marker, and never merge, remove, reorder, or move text across those markers. \");\n }\n if (mode == RequestMode.MAPPING_JSON) {\n prompt.append(\"When the user prompt is a structured mapping document, output JSON only and do not wrap the answer in markdown or code fences. \");\n prompt.append(\"The JSON shape must be {\\\"version\\\":\\\"mapping-v1\\\",\\\"jobId\\\":\\\"...\\\",\\\"translations\\\":[{\\\"id\\\":\\\"...\\\",\\\"translation\\\":\\\"...\\\"}]}. \");\n prompt.append(\"Return one translation entry for every unit id from the input. Do not change ids or order. \");\n prompt.append(\"@@@F@@@ markers are style placeholders owned by the client; every marker in a unit must appear exactly once in that unit's translation. \");\n prompt.append(\"For layoutMode=flexible-tooltip, each unit may be a semantic style block or an inline styled token. Translate each unit naturally as game localization, not word-by-word. You may change word order and line breaks, but every @@@F marker inside that same unit must appear exactly once. \");\n prompt.append(\"Do not translate a semantic block by following the source's visual line breaks; translate the block's meaning as a complete title, lore paragraph, operation hint, stat row, or mechanic sentence according to its role/tokenMask. \");\n prompt.append(\"For layoutMode=fixed-layout, preserve slot boundaries and line structure as instructed by the unit metadata. \");\n }\n if (mode == RequestMode.DIRECT_FORMATTED) {\n prompt.append(\"The user prompt is a reversible SimpleTranslate formatted document, optionally preceded by , , and sections. \");\n prompt.append(\"If the user prompt is wrapped in , return one with the same root attributes and every exactly once; each item's content must follow the same rules independently. \");\n prompt.append(\"Never move text, tags, or ids between entries. \");\n prompt.append(\"Use as surrounding context for signs, books, chat, and tooltips, but never return it. \");\n prompt.append(\"For surface=\\\"chat.context.direct\\\", lists chat history from oldest to newest and marks exactly one [target] line. Use [previous] lines only to understand references and tone; translate only the current target message, keep player names and chat prefixes stable, and never return previous context lines. \");\n prompt.append(\"For surface=\\\"chat.context.direct\\\" or \\\"chat.message.segment.direct\\\" with mode=\\\"line-text\\\", the current contains exactly one target chat line; return that same one only, or a single plain translated target line if tag output would be malformed. \");\n prompt.append(\"Use only to understand the complete visible sentence across styled runs; never return it. \");\n prompt.append(\"For mode=\\\"free\\\" tooltips and hover text, translate from the complete plain line context, not from isolated styled fragments; do not copy source-language words back as fallback when a phrase spans split runs. \");\n prompt.append(\"For surface=\\\"tooltip.item_context.direct\\\", is the complete numbered item tooltip block with line roles such as title, lore, mechanic, usage, hotkey, empty, and separator. Translate each returned using the entire item tooltip block as context, so multi-line lore and mechanics read consistently across adjacent lines. \");\n prompt.append(\"For surface=\\\"tooltip.item_context.direct\\\", do not treat lore or mechanic lines as isolated sentences. In mode=\\\"line-text\\\", preserve the exact line count, line indexes, and per-line boundaries, translate only the plain text inside each , and do not create , , or any other nested tags. \");\n prompt.append(\"For surface=\\\"hud.title_group.component.direct\\\", may contain recent title/subtitle command history, including original lines whose translated text is still [pending]. Use those prior originals and translations to keep fast subtitle-like title commands semantically continuous; never repeat or return the history. \");\n prompt.append(\"For surface=\\\"hud.actionbar.component.direct\\\", may contain recent title/actionbar caption history. Use it to keep consecutive actionbar subtitle fragments coherent, but keep numeric placeholder groups like @@0@@ exactly once each and do not invent or remove counters. \");\n prompt.append(\"For surface=\\\"hud.history.caption_batch.direct\\\", translate the batch as ordered HUD caption subtitles. Treat all current lines as consecutive fragments of one subtitle passage, not isolated sentences. When a sentence spans multiple lines, understand the whole sentence first, then distribute the target wording across the same line indexes in natural reading order. contains only captions before the current batch; use it for continuity, but do not let it change line count, line order, tag ids, or the exact @@0@@-style placeholders in the current . \");\n prompt.append(\"For surface=\\\"text_display.component.direct\\\", preserve every style slot exactly once so colored in-world labels, hotkeys, and decorative title letters keep their original colors and formatting. Translate short instruction/label phrases inside their own slots; keep already-localized hotkeys and stylized proper-name/logo text unchanged when translating it would break the visual title. \");\n prompt.append(\"Avoid bilingual output: translate natural-language source text to the target language unless it is already target-language text or a structural token represented by tags, numbers, ids, coordinates, or player names. \");\n prompt.append(\"Use to understand fullwidth or compatibility characters such as ATTEMPTING, but keep returned tag structure based on . Never return the normalized context section. \");\n prompt.append(\"If a section is present, obey every source -> target term exactly for matching visible UI/status words, and never return the glossary section. \");\n prompt.append(\"Return the document format only; do not output JSON, markdown, code fences, explanations, or bilingual text. \");\n prompt.append(\"Preserve the root attributes, every tag, every line index, and the line count exactly. \");\n prompt.append(\"If contains blocks, preserve every sign id exactly once. You may reorder sign blocks only if every id remains complete and unique; never move a line, run, command, number, gamerule, or sentence fragment from one sign id into another. \");\n prompt.append(\"For sign-group documents, every sign must still contain exactly four elements and every line must keep its original elements in place. \");\n prompt.append(\"For surface=\\\"sign.auto.single.direct\\\", the one sign's four lines are a single semantic sign message. Understand the full four-line block before translating any line; wrapped phrases, menu labels, instructions, and lore must read consistently across adjacent lines. \");\n prompt.append(\"For surface=\\\"sign.auto.single.direct\\\", do not translate each visual line as an isolated sentence. Keep the exact four line indexes and run ids, but distribute the target-language wording across those same line slots in natural display order. \");\n prompt.append(\"If mode=\\\"line-text\\\", never add, remove, reorder, split, or merge elements; translate only the text content of each and keep empty, separator, numeric, icon-only, and pure hotkey/control-token lines unchanged when they have no natural-language meaning. \");\n prompt.append(\"If mode=\\\"fixed\\\", never add, remove, reorder, split, or merge elements; translate only editable=\\\"true\\\" run text in place and copy editable=\\\"false\\\" run text exactly. \");\n prompt.append(\"If mode=\\\"free\\\", translate the full line naturally and you may move tags inside the same line to match target-language word order. \");\n prompt.append(\"For mode=\\\"free\\\", tags normally contain only id attributes. Every original from that line must appear exactly once in the returned same line, ids must be unchanged, and no new ids may be created. \");\n prompt.append(\"For mode=\\\"free\\\", text outside tags is allowed for natural grammar and will use the line's base style; put styled words, numbers, key terms, and moved tokens inside their original tags. Do not add style/editable attributes to tags. \");\n prompt.append(\"Translate bracketed UI labels and title-case system labels such as [Animate Heroes], [Enemies], Abbreviations, and Score Leaderboard; do not treat them as protected player names. \");\n prompt.append(\"If an editable tag contains only a short English UI/status phrase, translate that phrase instead of copying it unchanged. \");\n prompt.append(\"When source words are split across adjacent styled runs, translate according to the complete plain context instead of preserving English order. \");\n prompt.append(\"Example: 'draining 15 Mana every 0.4' should become '每 0.4 秒消耗 15 点 法力', not '每 15 秒消耗 0.4 法力'. \");\n prompt.append(\"Do not change XML entities except as needed for valid escaped text. \");\n prompt.append(\"Keep colors, styles, click events, hover events, icons, numbers, ids, and player names represented by tags/attributes untouched; translate all visible English text inside editable tags. \");\n }\n\n String stylePrompt = ModConfig.TRANSLATION_STYLE_PROMPT.get();\n if (stylePrompt != null) {\n stylePrompt = stylePrompt.trim();\n if (!stylePrompt.isEmpty()) {\n prompt.append(\"\\n\\nFollow this custom translation style instruction strictly: \");\n prompt.append(stylePrompt);\n prompt.append(\" \");\n prompt.append(\"Apply it to tone and wording, but never break formatting, control markers, or line structure.\");\n }\n }\n\n if (!termHints.isEmpty()) {\n prompt.append(\"\\n\\nUse these term translations for consistency:\\n\");\n for (TermHint hint : termHints) {\n prompt.append(\"- \\\"\").append(hint.original()).append(\"\\\" -> \\\"\").append(hint.translation())\n .append(\"\\\"\\n\");\n }\n }\n\n return prompt.toString();\n }\n\n private String buildMappingUserPrompt(TranslationMapping mapping, String sourceLanguage, String targetLanguage) {\n JsonObject root = new JsonObject();\n root.addProperty(\"version\", \"mapping-v1\");\n root.addProperty(\"jobId\", mapping.taskId());\n root.addProperty(\"surface\", mapping.surface());\n root.addProperty(\"sourceLanguage\", getLanguageName(sourceLanguage));\n root.addProperty(\"targetLanguage\", getLanguageName(targetLanguage));\n root.addProperty(\"context\", mapping.context());\n root.addProperty(\"layoutSignature\", mapping.layoutSignature());\n root.addProperty(\"styleSignature\", mapping.styleSignature());\n boolean flexibleLayout = isFlexibleMappingSurface(mapping.surface());\n root.addProperty(\"layoutMode\", flexibleLayout ? \"flexible-tooltip\" : \"fixed-layout\");\n root.addProperty(\"instructions\", flexibleLayout\n ? \"Translate every unit as its own semantic style block or styled token. Use natural Minecraft localization for the target language, ignore source visual line breaks, keep every @@@F marker exactly once in the same unit, and preserve only structural tokens such as numbers, ids, commands, control codes, and placeholders.\"\n : \"Translate each fixed-layout slot conservatively. Preserve line/slot structure, technical tokens, and every @@@F marker exactly.\");\n\n JsonArray units = new JsonArray();\n for (TranslationSlot slot : mapping.slots()) {\n JsonObject unit = new JsonObject();\n unit.addProperty(\"id\", slot.id());\n unit.addProperty(\"text\", slot.text());\n unit.addProperty(\"role\", slot.role());\n unit.addProperty(\"maxChars\", slot.maxWidth());\n unit.addProperty(\"fixed\", slot.fixedSlot());\n unit.addProperty(\"lineIndex\", slot.lineIndex());\n unit.addProperty(\"slotIndex\", slot.slotIndex());\n unit.addProperty(\"styleSignature\", slot.styleSignature());\n unit.addProperty(\"tokenMask\", slot.tokenMask());\n units.add(unit);\n }\n root.add(\"units\", units);\n return root.toString();\n }\n\n private boolean isFlexibleMappingSurface(String surface) {\n if (surface == null) {\n return false;\n }\n String normalized = surface.toLowerCase();\n return normalized.startsWith(\"tooltip.\")\n || normalized.startsWith(\"hover.\")\n || normalized.startsWith(\"book.\");\n }\n\n private String getLanguageName(String code) {\n return TranslationTextDetector.displayLanguageName(code);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt) {\n return sendRequest(systemPrompt, userPrompt, estimateMaxTokens(userPrompt));\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens) {\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\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 surface = detectRequestSurface(userPrompt);\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestBlocking(profiles, apiKey, model, systemPrompt, userPrompt, maxTokens));\n }\n\n private String sendRequestBlocking(List profiles, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n for (ApiRequestProfile profile : orderedProfiles) {\n String translated = sendRequestWithProfile(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return translated;\n }\n }\n return null;\n }\n\n private String sendRequestWithProfile(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return null;\n }\n\n long startedAt = System.nanoTime();\n try {\n HttpRequest request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n String result = parseResponse(response.body(), startedAt);\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 return result;\n }\n SimpleTranslateMod.getLogger().warn(\"Empty translation result from DeepSeek\");\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 (InterruptedException e) {\n Thread.currentThread().interrupt();\n SimpleTranslateMod.getLogger().error(\"Translation request interrupted\", e);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n throw e;\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\", e.getMessage(), e);\n return null;\n }\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 String detectRequestSurface(String userPrompt) {\n if (userPrompt == null || userPrompt.isBlank()) {\n return \"manager.raw\";\n }\n Matcher direct = DIRECT_SURFACE_PATTERN.matcher(userPrompt);\n if (direct.find()) {\n return direct.group(1);\n }\n Matcher mapping = MAPPING_SURFACE_PATTERN.matcher(userPrompt);\n if (mapping.find()) {\n return mapping.group(1);\n }\n return \"manager.raw\";\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 static int estimateMaxTokens(String text) {\n int length = text == null ? 0 : text.length();\n int estimated = length + 512;\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n return MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n return Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.contains(\"data:\")) {\n return parseStreamingResponse(responseBody);\n }\n return parsePlainResponse(responseBody);\n }\n\n private String parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return null;\n }\n\n Future readerTask = 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 null;\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 String parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return null;\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\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 null;\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);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return null;\n }\n\n if (sawData) {\n return ModelOutputSanitizer.sanitize(result.toString());\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 String parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\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);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return null;\n }\n return ModelOutputSanitizer.sanitize(result.toString());\n }\n\n private String parsePlainResponse(String responseBody) {\n try {\n JsonObject response = JsonParser.parseString(responseBody).getAsJsonObject();\n String raw = extractAssistantContent(response);\n if (raw != null) {\n return ModelOutputSanitizer.sanitize(raw);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse non-stream response: {}\", responseBody, e);\n }\n return null;\n }\n\n private TranslationMappingResult parseMappingResponse(String response, TranslationMapping mapping) {\n String sanitized = ModelOutputSanitizer.sanitize(response);\n if (sanitized == null || sanitized.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n try {\n String jsonPayload = extractJsonObjectPayload(sanitized);\n if (jsonPayload == null || jsonPayload.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n JsonObject root = JsonParser.parseString(jsonPayload).getAsJsonObject();\n String version = root.has(\"version\") ? root.get(\"version\").getAsString() : \"\";\n String jobId = root.has(\"jobId\") ? root.get(\"jobId\").getAsString() : \"\";\n if (!\"mapping-v1\".equals(version) || !mapping.taskId().equals(jobId)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n if (!root.has(\"translations\") || !root.get(\"translations\").isJsonArray()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n Map translations = new LinkedHashMap<>();\n for (var element : root.getAsJsonArray(\"translations\")) {\n if (!element.isJsonObject()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n JsonObject item = element.getAsJsonObject();\n String id = item.has(\"id\") ? item.get(\"id\").getAsString() : \"\";\n String translation = item.has(\"translation\") ? item.get(\"translation\").getAsString() : \"\";\n if (id.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n String cleaned = ModelOutputSanitizer.sanitize(translation);\n if (cleaned == null || cleaned.isBlank() || containsDisallowedMappingTokens(cleaned)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n translations.put(id, cleaned);\n }\n\n if (translations.size() != mapping.slots().size()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n for (TranslationSlot slot : mapping.slots()) {\n if (!translations.containsKey(slot.id())) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n if (slot.fixedSlot()) {\n String normalizedSource = TranslationCacheKeys.normalizeSource(slot.text());\n String normalizedTranslated = TranslationCacheKeys.normalizeSource(translations.get(slot.id()));\n if (!normalizedSource.equals(normalizedTranslated)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n }\n }\n\n return new TranslationMappingResult(mapping.taskId(), translations);\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().debug(\"Failed to parse mapping response\", e);\n return new TranslationMappingResult(mapping.taskId(), Map.of());\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 boolean containsDisallowedMappingTokens(String text) {\n if (text == null || text.isEmpty()) {\n return false;\n }\n return containsDisallowedMappingMarker(text)\n || text.contains(\"```\")\n || text.contains(\"\")\n || text.contains(\"\")\n || text.contains(\"\")\n || text.contains(\"\");\n }\n\n private boolean containsDisallowedMappingMarker(String text) {\n int index = text.indexOf(\"@@@\");\n if (index < 0) {\n return false;\n }\n Matcher matcher = ALLOWED_MAPPING_PLACEHOLDER_PATTERN.matcher(text);\n int cursor = 0;\n while (matcher.find()) {\n String between = text.substring(cursor, matcher.start());\n if (between.contains(\"@@@\")) {\n return true;\n }\n cursor = matcher.end();\n }\n return text.substring(cursor).contains(\"@@@\");\n }\n\n private void appendStreamingChunk(StringBuilder result, String json) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\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 TranslationService.ApiDetectionResult 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 = sendRequestWithProfile(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationService.ApiDetectionResult(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 TranslationService.ApiDetectionResult(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationService.ModelAccessResult 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\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, \"Connectivity probe.\", \"Hi\", 8, false))))\n .timeout(Duration.ofSeconds(30));\n applyAuthHeader(builder, profile, apiKey);\n\n HttpResponse response = httpClient.send(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream());\n lastStatus = response.statusCode();\n String body = readErrorBody(response.body());\n if (response.statusCode() == 200) {\n lastSuccessfulProfile = profile;\n return new TranslationService.ModelAccessResult(true, modelId, response.statusCode(),\n \"Model verified\");\n }\n lastMessage = body == null || body.isBlank() ? (\"HTTP \" + response.statusCode()) : body;\n } catch (InterruptedException e) {\n Thread.currentThread().interrupt();\n return new TranslationService.ModelAccessResult(false, modelId, 0, \"Model verification interrupted\");\n } catch (IOException | RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationService.ModelAccessResult(false, modelId, lastStatus, lastMessage);\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, false));\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 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(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 requestBody.addProperty(\"max_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n if (!thinkingEnabled) {\n requestBody.addProperty(\"temperature\", 0.1);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\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 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 requestBody.addProperty(\"max_output_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n requestBody.addProperty(\"temperature\", 0.1);\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 requestBody.addProperty(\"max_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n requestBody.addProperty(\"temperature\", 0.1);\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 systemPrompt, String userPrompt, 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.1);\n generationConfig.addProperty(\"maxOutputTokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\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 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 @Override\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n @Override\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.1/src/main/java/com/yourname/simpletranslate/translation/DeepSeekTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067442.8884249, "n": 60896, "h": "4db9c81e047177fe", "c": "package com.yourname.simpletranslate.translation;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.util.ModelOutputSanitizer;\nimport com.yourname.simpletranslate.util.TranslationCacheKeys;\nimport com.yourname.simpletranslate.util.TranslationMapping;\nimport com.yourname.simpletranslate.util.TranslationMappingResult;\nimport com.yourname.simpletranslate.util.TranslationSlot;\nimport com.yourname.simpletranslate.util.TranslationTextDetector;\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.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 private static final Pattern ALLOWED_MAPPING_PLACEHOLDER_PATTERN = Pattern.compile(\"@@@F\\\\d+@@@\");\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 = 8192;\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 DIRECT_SURFACE_PATTERN = Pattern.compile(\"]*\\\\bsurface=\\\"([^\\\"]+)\\\"\");\n private static final Pattern MAPPING_SURFACE_PATTERN = Pattern.compile(\"\\\"surface\\\"\\\\s*:\\\\s*\\\"([^\\\"]+)\\\"\");\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 /** Alias so existing references keep working after the prompt-builder split. */\n private static final class RequestMode {\n static final TranslationPrompts.RequestMode PLAIN_TEXT = TranslationPrompts.RequestMode.PLAIN_TEXT;\n static final TranslationPrompts.RequestMode PRESERVE_MARKERS = TranslationPrompts.RequestMode.PRESERVE_MARKERS;\n static final TranslationPrompts.RequestMode MAPPING_JSON = TranslationPrompts.RequestMode.MAPPING_JSON;\n static final TranslationPrompts.RequestMode DIRECT_FORMATTED = TranslationPrompts.RequestMode.DIRECT_FORMATTED;\n\n private RequestMode() {\n }\n }\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 @Override\n public CompletableFuture translate(String text, String sourceLanguage, String targetLanguage) {\n return translateWithTerms(text, sourceLanguage, targetLanguage, List.of());\n }\n\n @Override\n public CompletableFuture translateWithTerms(String text, String sourceLanguage, String targetLanguage,\n List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.PLAIN_TEXT);\n return sendRequest(systemPrompt, text);\n }\n\n @Override\n public CompletableFuture translatePreservingMarkers(String text, String sourceLanguage,\n String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.PRESERVE_MARKERS);\n return sendRequest(systemPrompt, text);\n }\n\n @Override\n public CompletableFuture translateFormattedDocument(String document, String surface,\n String sourceLanguage, String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n // Style/term hints live in the user payload so the system prompt stays\n // byte-stable per language pair (provider prefix caching).\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, List.of(),\n RequestMode.DIRECT_FORMATTED);\n String userPrompt = insertDirectSections(document, TranslationPrompts.directUserSections(termHints));\n return sendRequest(systemPrompt, userPrompt, estimateDirectMaxTokens(document), surface);\n }\n\n /** Same as {@link #translateFormattedDocument} but scales the output token budget. */\n public CompletableFuture translateFormattedDocument(String document, String surface,\n String sourceLanguage, String targetLanguage, List termHints, int maxTokenMultiplier) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, List.of(),\n RequestMode.DIRECT_FORMATTED);\n String userPrompt = insertDirectSections(document, TranslationPrompts.directUserSections(termHints));\n return sendRequest(systemPrompt, userPrompt, estimateDirectMaxTokens(document, maxTokenMultiplier), surface);\n }\n\n /** Inserts [STYLE]/[TERMS] sections right before the [TEXT] block. */\n private static String insertDirectSections(String document, String sections) {\n if (sections == null || sections.isBlank() || document == null) {\n return document;\n }\n int index = document.indexOf(\"[TEXT\");\n if (index < 0) {\n return sections + document;\n }\n return document.substring(0, index) + sections + document.substring(index);\n }\n\n /** Output budget follows the [TEXT] block size, not the whole payload with context. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Output budget follows the [TEXT] block; multiplier supports partial retries. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int start = body.indexOf(\"[TEXT\");\n int end = body.lastIndexOf(\"[/TEXT]\");\n if (start >= 0 && end > start) {\n body = body.substring(start, end);\n }\n int lineCount = 0;\n for (String line : body.split(\"\\\\R\")) {\n if (line != null && line.matches(\"(?s).*?\\\\d{1,4}\\\\s*[||].*\")) {\n lineCount++;\n }\n }\n // Chinese game text often expands; multi-line tooltips need headroom per line.\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int lineEstimate = lineCount * 192 + 384;\n int estimated = Math.max(charEstimate, lineEstimate);\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 @Override\n public CompletableFuture translateMapping(TranslationMapping mapping,\n String sourceLanguage, String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.MAPPING_JSON);\n String userPrompt = buildMappingUserPrompt(mapping, sourceLanguage, targetLanguage);\n return sendRequest(systemPrompt, userPrompt).thenApply(raw -> parseMappingResponse(raw, mapping));\n }\n\n @Override\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationService.ApiDetectionResult(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 @Override\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationService.ModelDetectionResult(\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 @Override\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 TranslationService.ModelAccessResult(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationService.ModelAccessResult(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationService.ModelAccessResult(\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 String buildSystemPrompt(String sourceLanguage, String targetLanguage, List termHints,\n TranslationPrompts.RequestMode mode) {\n return TranslationPrompts.buildSystemPrompt(sourceLanguage, targetLanguage, termHints, mode);\n }\n\n\n private String buildMappingUserPrompt(TranslationMapping mapping, String sourceLanguage, String targetLanguage) {\n JsonObject root = new JsonObject();\n root.addProperty(\"version\", \"mapping-v1\");\n root.addProperty(\"jobId\", mapping.taskId());\n root.addProperty(\"surface\", mapping.surface());\n root.addProperty(\"sourceLanguage\", getLanguageName(sourceLanguage));\n root.addProperty(\"targetLanguage\", getLanguageName(targetLanguage));\n root.addProperty(\"context\", mapping.context());\n root.addProperty(\"layoutSignature\", mapping.layoutSignature());\n root.addProperty(\"styleSignature\", mapping.styleSignature());\n boolean flexibleLayout = isFlexibleMappingSurface(mapping.surface());\n root.addProperty(\"layoutMode\", flexibleLayout ? \"flexible-tooltip\" : \"fixed-layout\");\n root.addProperty(\"instructions\", flexibleLayout\n ? \"Translate every unit as its own semantic style block or styled token. Use natural Minecraft localization for the target language, ignore source visual line breaks, keep every @@@F marker exactly once in the same unit, and preserve only structural tokens such as numbers, ids, commands, control codes, and placeholders.\"\n : \"Translate each fixed-layout slot conservatively. Preserve line/slot structure, technical tokens, and every @@@F marker exactly.\");\n\n JsonArray units = new JsonArray();\n for (TranslationSlot slot : mapping.slots()) {\n JsonObject unit = new JsonObject();\n unit.addProperty(\"id\", slot.id());\n unit.addProperty(\"text\", slot.text());\n unit.addProperty(\"role\", slot.role());\n unit.addProperty(\"maxChars\", slot.maxWidth());\n unit.addProperty(\"fixed\", slot.fixedSlot());\n unit.addProperty(\"lineIndex\", slot.lineIndex());\n unit.addProperty(\"slotIndex\", slot.slotIndex());\n unit.addProperty(\"styleSignature\", slot.styleSignature());\n unit.addProperty(\"tokenMask\", slot.tokenMask());\n units.add(unit);\n }\n root.add(\"units\", units);\n return root.toString();\n }\n\n private boolean isFlexibleMappingSurface(String surface) {\n if (surface == null) {\n return false;\n }\n String normalized = surface.toLowerCase();\n return normalized.startsWith(\"tooltip.\")\n || normalized.startsWith(\"hover.\")\n || normalized.startsWith(\"book.\");\n }\n\n private String getLanguageName(String code) {\n return TranslationTextDetector.displayLanguageName(code);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt) {\n return sendRequest(systemPrompt, userPrompt, estimateMaxTokens(userPrompt));\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens) {\n return sendRequest(systemPrompt, userPrompt, maxTokens, null);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String explicitSurface) {\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 surface = explicitSurface == null || explicitSurface.isBlank()\n ? detectRequestSurface(userPrompt)\n : explicitSurface;\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestBlocking(profiles, apiKey, model, systemPrompt, userPrompt, maxTokens));\n }\n\n private String sendRequestBlocking(List profiles, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n for (ApiRequestProfile profile : orderedProfiles) {\n String translated = sendRequestWithProfile(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return translated;\n }\n }\n return null;\n }\n\n private String sendRequestWithProfile(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return null;\n }\n\n long startedAt = System.nanoTime();\n try {\n HttpRequest request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n String result = parseResponse(response.body(), startedAt);\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 return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\"Empty translation result from DeepSeek\");\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 (InterruptedException e) {\n Thread.currentThread().interrupt();\n SimpleTranslateMod.getLogger().error(\"Translation request interrupted\", e);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n throw e;\n } catch (InvalidApiResponseException e) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\", e.getMessage());\n return null;\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\", e.getMessage(), e);\n return null;\n }\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 String detectRequestSurface(String userPrompt) {\n if (userPrompt == null || userPrompt.isBlank()) {\n return \"manager.raw\";\n }\n Matcher direct = DIRECT_SURFACE_PATTERN.matcher(userPrompt);\n if (direct.find()) {\n return direct.group(1);\n }\n Matcher mapping = MAPPING_SURFACE_PATTERN.matcher(userPrompt);\n if (mapping.find()) {\n return mapping.group(1);\n }\n return \"manager.raw\";\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 static int estimateMaxTokens(String text) {\n int length = text == null ? 0 : text.length();\n int estimated = length + 512;\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n return MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n return Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.contains(\"data:\")) {\n return parseStreamingResponse(responseBody);\n }\n return parsePlainResponse(responseBody);\n }\n\n private String parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return null;\n }\n\n Future readerTask = 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 null;\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 String parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return null;\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\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 null;\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);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return null;\n }\n\n if (sawData) {\n return ModelOutputSanitizer.sanitize(result.toString());\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 String parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\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);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return null;\n }\n return ModelOutputSanitizer.sanitize(result.toString());\n }\n\n private String parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\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 ModelOutputSanitizer.sanitize(raw);\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 TranslationMappingResult parseMappingResponse(String response, TranslationMapping mapping) {\n String sanitized = ModelOutputSanitizer.sanitize(response);\n if (sanitized == null || sanitized.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n try {\n String jsonPayload = extractJsonObjectPayload(sanitized);\n if (jsonPayload == null || jsonPayload.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n JsonObject root = JsonParser.parseString(jsonPayload).getAsJsonObject();\n String version = root.has(\"version\") ? root.get(\"version\").getAsString() : \"\";\n String jobId = root.has(\"jobId\") ? root.get(\"jobId\").getAsString() : \"\";\n if (!\"mapping-v1\".equals(version) || !mapping.taskId().equals(jobId)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n if (!root.has(\"translations\") || !root.get(\"translations\").isJsonArray()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n Map translations = new LinkedHashMap<>();\n for (var element : root.getAsJsonArray(\"translations\")) {\n if (!element.isJsonObject()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n JsonObject item = element.getAsJsonObject();\n String id = item.has(\"id\") ? item.get(\"id\").getAsString() : \"\";\n String translation = item.has(\"translation\") ? item.get(\"translation\").getAsString() : \"\";\n if (id.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n String cleaned = ModelOutputSanitizer.sanitize(translation);\n if (cleaned == null || cleaned.isBlank() || containsDisallowedMappingTokens(cleaned)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n translations.put(id, cleaned);\n }\n\n if (translations.size() != mapping.slots().size()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n for (TranslationSlot slot : mapping.slots()) {\n if (!translations.containsKey(slot.id())) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n if (slot.fixedSlot()) {\n String normalizedSource = TranslationCacheKeys.normalizeSource(slot.text());\n String normalizedTranslated = TranslationCacheKeys.normalizeSource(translations.get(slot.id()));\n if (!normalizedSource.equals(normalizedTranslated)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n }\n }\n\n return new TranslationMappingResult(mapping.taskId(), translations);\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().debug(\"Failed to parse mapping response\", e);\n return new TranslationMappingResult(mapping.taskId(), Map.of());\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 boolean containsDisallowedMappingTokens(String text) {\n if (text == null || text.isEmpty()) {\n return false;\n }\n return containsDisallowedMappingMarker(text)\n || text.contains(\"```\")\n || text.contains(\"\")\n || text.contains(\"\")\n || text.contains(\"\")\n || text.contains(\"\");\n }\n\n private boolean containsDisallowedMappingMarker(String text) {\n int index = text.indexOf(\"@@@\");\n if (index < 0) {\n return false;\n }\n Matcher matcher = ALLOWED_MAPPING_PLACEHOLDER_PATTERN.matcher(text);\n int cursor = 0;\n while (matcher.find()) {\n String between = text.substring(cursor, matcher.start());\n if (between.contains(\"@@@\")) {\n return true;\n }\n cursor = matcher.end();\n }\n return text.substring(cursor).contains(\"@@@\");\n }\n\n private void appendStreamingChunk(StringBuilder result, String json) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\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 TranslationService.ApiDetectionResult 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 = sendRequestWithProfile(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationService.ApiDetectionResult(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 TranslationService.ApiDetectionResult(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationService.ModelAccessResult 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 = buildSystemPrompt(\"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(),\n RequestMode.DIRECT_FORMATTED);\n String probeUserPrompt = \"\"\"\n \n [TEXT lines=1]\n 0|Hello\n [/TEXT]\n \n \"\"\";\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.send(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream());\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 TranslationService.ModelAccessResult(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 (InterruptedException e) {\n Thread.currentThread().interrupt();\n return new TranslationService.ModelAccessResult(false, modelId, 0, \"Model verification interrupted\");\n } catch (IOException | RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationService.ModelAccessResult(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n for (String line : value.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (trimmed.matches(\"0\\\\s*[||].+\")) {\n return true;\n }\n }\n return false;\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, false));\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 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(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 requestBody.addProperty(\"max_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n if (!thinkingEnabled) {\n requestBody.addProperty(\"temperature\", 0.1);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\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 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 requestBody.addProperty(\"max_output_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n requestBody.addProperty(\"temperature\", 0.1);\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 requestBody.addProperty(\"max_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n requestBody.addProperty(\"temperature\", 0.1);\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 systemPrompt, String userPrompt, 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.1);\n generationConfig.addProperty(\"maxOutputTokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\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 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 @Override\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n @Override\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "neoforge/SimpleTranslate-NeoForge-1.21.5/src/main/java/com/yourname/simpletranslate/translation/DeepSeekTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067442.945994, "n": 60896, "h": "4db9c81e047177fe", "c": "package com.yourname.simpletranslate.translation;\n\nimport com.google.gson.Gson;\nimport com.google.gson.JsonArray;\nimport com.google.gson.JsonObject;\nimport com.google.gson.JsonParser;\nimport com.yourname.simpletranslate.SimpleTranslateMod;\nimport com.yourname.simpletranslate.config.ModConfig;\nimport com.yourname.simpletranslate.util.ModelOutputSanitizer;\nimport com.yourname.simpletranslate.util.TranslationCacheKeys;\nimport com.yourname.simpletranslate.util.TranslationMapping;\nimport com.yourname.simpletranslate.util.TranslationMappingResult;\nimport com.yourname.simpletranslate.util.TranslationSlot;\nimport com.yourname.simpletranslate.util.TranslationTextDetector;\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.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 private static final Pattern ALLOWED_MAPPING_PLACEHOLDER_PATTERN = Pattern.compile(\"@@@F\\\\d+@@@\");\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 = 8192;\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 DIRECT_SURFACE_PATTERN = Pattern.compile(\"]*\\\\bsurface=\\\"([^\\\"]+)\\\"\");\n private static final Pattern MAPPING_SURFACE_PATTERN = Pattern.compile(\"\\\"surface\\\"\\\\s*:\\\\s*\\\"([^\\\"]+)\\\"\");\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 /** Alias so existing references keep working after the prompt-builder split. */\n private static final class RequestMode {\n static final TranslationPrompts.RequestMode PLAIN_TEXT = TranslationPrompts.RequestMode.PLAIN_TEXT;\n static final TranslationPrompts.RequestMode PRESERVE_MARKERS = TranslationPrompts.RequestMode.PRESERVE_MARKERS;\n static final TranslationPrompts.RequestMode MAPPING_JSON = TranslationPrompts.RequestMode.MAPPING_JSON;\n static final TranslationPrompts.RequestMode DIRECT_FORMATTED = TranslationPrompts.RequestMode.DIRECT_FORMATTED;\n\n private RequestMode() {\n }\n }\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 @Override\n public CompletableFuture translate(String text, String sourceLanguage, String targetLanguage) {\n return translateWithTerms(text, sourceLanguage, targetLanguage, List.of());\n }\n\n @Override\n public CompletableFuture translateWithTerms(String text, String sourceLanguage, String targetLanguage,\n List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.PLAIN_TEXT);\n return sendRequest(systemPrompt, text);\n }\n\n @Override\n public CompletableFuture translatePreservingMarkers(String text, String sourceLanguage,\n String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.PRESERVE_MARKERS);\n return sendRequest(systemPrompt, text);\n }\n\n @Override\n public CompletableFuture translateFormattedDocument(String document, String surface,\n String sourceLanguage, String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n // Style/term hints live in the user payload so the system prompt stays\n // byte-stable per language pair (provider prefix caching).\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, List.of(),\n RequestMode.DIRECT_FORMATTED);\n String userPrompt = insertDirectSections(document, TranslationPrompts.directUserSections(termHints));\n return sendRequest(systemPrompt, userPrompt, estimateDirectMaxTokens(document), surface);\n }\n\n /** Same as {@link #translateFormattedDocument} but scales the output token budget. */\n public CompletableFuture translateFormattedDocument(String document, String surface,\n String sourceLanguage, String targetLanguage, List termHints, int maxTokenMultiplier) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, List.of(),\n RequestMode.DIRECT_FORMATTED);\n String userPrompt = insertDirectSections(document, TranslationPrompts.directUserSections(termHints));\n return sendRequest(systemPrompt, userPrompt, estimateDirectMaxTokens(document, maxTokenMultiplier), surface);\n }\n\n /** Inserts [STYLE]/[TERMS] sections right before the [TEXT] block. */\n private static String insertDirectSections(String document, String sections) {\n if (sections == null || sections.isBlank() || document == null) {\n return document;\n }\n int index = document.indexOf(\"[TEXT\");\n if (index < 0) {\n return sections + document;\n }\n return document.substring(0, index) + sections + document.substring(index);\n }\n\n /** Output budget follows the [TEXT] block size, not the whole payload with context. */\n private static int estimateDirectMaxTokens(String document) {\n return estimateDirectMaxTokens(document, 1);\n }\n\n /** Output budget follows the [TEXT] block; multiplier supports partial retries. */\n static int estimateDirectMaxTokens(String document, int multiplier) {\n String body = document == null ? \"\" : document;\n int start = body.indexOf(\"[TEXT\");\n int end = body.lastIndexOf(\"[/TEXT]\");\n if (start >= 0 && end > start) {\n body = body.substring(start, end);\n }\n int lineCount = 0;\n for (String line : body.split(\"\\\\R\")) {\n if (line != null && line.matches(\"(?s).*?\\\\d{1,4}\\\\s*[||].*\")) {\n lineCount++;\n }\n }\n // Chinese game text often expands; multi-line tooltips need headroom per line.\n int charEstimate = (int) (body.length() * 2.4) + 512;\n int lineEstimate = lineCount * 192 + 384;\n int estimated = Math.max(charEstimate, lineEstimate);\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 @Override\n public CompletableFuture translateMapping(TranslationMapping mapping,\n String sourceLanguage, String targetLanguage, List termHints) {\n if (!isReady()) {\n return CompletableFuture.failedFuture(new IllegalStateException(\"DeepSeek API key not configured\"));\n }\n\n String systemPrompt = buildSystemPrompt(sourceLanguage, targetLanguage, termHints,\n RequestMode.MAPPING_JSON);\n String userPrompt = buildMappingUserPrompt(mapping, sourceLanguage, targetLanguage);\n return sendRequest(systemPrompt, userPrompt).thenApply(raw -> parseMappingResponse(raw, mapping));\n }\n\n @Override\n public CompletableFuture detectApi() {\n if (!isReady()) {\n return CompletableFuture.completedFuture(\n new TranslationService.ApiDetectionResult(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 @Override\n public CompletableFuture detectAvailableModels(String apiKey, String apiUrl,\n ModConfig.ApiFormat apiFormat) {\n return CompletableFuture.completedFuture(new TranslationService.ModelDetectionResult(\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 @Override\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 TranslationService.ModelAccessResult(\n false, model, 0, \"API key not configured\"));\n }\n if (model.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationService.ModelAccessResult(\n false, model, 0, \"Model ID not configured\"));\n }\n String urlError = ModConfig.validateApiUrl(apiUrl);\n if (!urlError.isBlank()) {\n return CompletableFuture.completedFuture(new TranslationService.ModelAccessResult(\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 String buildSystemPrompt(String sourceLanguage, String targetLanguage, List termHints,\n TranslationPrompts.RequestMode mode) {\n return TranslationPrompts.buildSystemPrompt(sourceLanguage, targetLanguage, termHints, mode);\n }\n\n\n private String buildMappingUserPrompt(TranslationMapping mapping, String sourceLanguage, String targetLanguage) {\n JsonObject root = new JsonObject();\n root.addProperty(\"version\", \"mapping-v1\");\n root.addProperty(\"jobId\", mapping.taskId());\n root.addProperty(\"surface\", mapping.surface());\n root.addProperty(\"sourceLanguage\", getLanguageName(sourceLanguage));\n root.addProperty(\"targetLanguage\", getLanguageName(targetLanguage));\n root.addProperty(\"context\", mapping.context());\n root.addProperty(\"layoutSignature\", mapping.layoutSignature());\n root.addProperty(\"styleSignature\", mapping.styleSignature());\n boolean flexibleLayout = isFlexibleMappingSurface(mapping.surface());\n root.addProperty(\"layoutMode\", flexibleLayout ? \"flexible-tooltip\" : \"fixed-layout\");\n root.addProperty(\"instructions\", flexibleLayout\n ? \"Translate every unit as its own semantic style block or styled token. Use natural Minecraft localization for the target language, ignore source visual line breaks, keep every @@@F marker exactly once in the same unit, and preserve only structural tokens such as numbers, ids, commands, control codes, and placeholders.\"\n : \"Translate each fixed-layout slot conservatively. Preserve line/slot structure, technical tokens, and every @@@F marker exactly.\");\n\n JsonArray units = new JsonArray();\n for (TranslationSlot slot : mapping.slots()) {\n JsonObject unit = new JsonObject();\n unit.addProperty(\"id\", slot.id());\n unit.addProperty(\"text\", slot.text());\n unit.addProperty(\"role\", slot.role());\n unit.addProperty(\"maxChars\", slot.maxWidth());\n unit.addProperty(\"fixed\", slot.fixedSlot());\n unit.addProperty(\"lineIndex\", slot.lineIndex());\n unit.addProperty(\"slotIndex\", slot.slotIndex());\n unit.addProperty(\"styleSignature\", slot.styleSignature());\n unit.addProperty(\"tokenMask\", slot.tokenMask());\n units.add(unit);\n }\n root.add(\"units\", units);\n return root.toString();\n }\n\n private boolean isFlexibleMappingSurface(String surface) {\n if (surface == null) {\n return false;\n }\n String normalized = surface.toLowerCase();\n return normalized.startsWith(\"tooltip.\")\n || normalized.startsWith(\"hover.\")\n || normalized.startsWith(\"book.\");\n }\n\n private String getLanguageName(String code) {\n return TranslationTextDetector.displayLanguageName(code);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt) {\n return sendRequest(systemPrompt, userPrompt, estimateMaxTokens(userPrompt));\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens) {\n return sendRequest(systemPrompt, userPrompt, maxTokens, null);\n }\n\n private CompletableFuture sendRequest(String systemPrompt, String userPrompt, int maxTokens,\n String explicitSurface) {\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 surface = explicitSurface == null || explicitSurface.isBlank()\n ? detectRequestSurface(userPrompt)\n : explicitSurface;\n String queueKey = buildQueueKey(apiUrl, model, apiFormat.name(), systemPrompt, userPrompt, maxTokens);\n return TranslationRequestQueue.submit(queueKey, surface, priorityForSurface(surface), MAX_REQUEST_ATTEMPTS,\n () -> sendRequestBlocking(profiles, apiKey, model, systemPrompt, userPrompt, maxTokens));\n }\n\n private String sendRequestBlocking(List profiles, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n List orderedProfiles = profiles == null ? List.of() : profiles;\n for (ApiRequestProfile profile : orderedProfiles) {\n String translated = sendRequestWithProfile(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n if (translated != null && !translated.isBlank()) {\n lastSuccessfulProfile = profile;\n return translated;\n }\n }\n return null;\n }\n\n private String sendRequestWithProfile(ApiRequestProfile profile, String apiKey, String model,\n String systemPrompt, String userPrompt, int maxTokens) {\n if (profile == null || profile.endpointUrl() == null || profile.endpointUrl().isBlank()) {\n return null;\n }\n\n long startedAt = System.nanoTime();\n try {\n HttpRequest request = buildRequest(profile, apiKey, model, systemPrompt, userPrompt, maxTokens);\n HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofInputStream());\n SimpleTranslateMod.getLogger().debug(\"Received DeepSeek response with status: {}\",\n response.statusCode());\n if (response.statusCode() == 200) {\n String result = parseResponse(response.body(), startedAt);\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 return result;\n }\n throw new TranslationRequestQueue.RetryableTranslationException(\"Empty translation result from DeepSeek\");\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 (InterruptedException e) {\n Thread.currentThread().interrupt();\n SimpleTranslateMod.getLogger().error(\"Translation request interrupted\", e);\n return null;\n } catch (IOException e) {\n throw new TranslationRequestQueue.RetryableTranslationException(\n e.getMessage() == null ? \"I/O error\" : e.getMessage(), e);\n } catch (TranslationRequestQueue.RetryableTranslationException e) {\n throw e;\n } catch (InvalidApiResponseException e) {\n SimpleTranslateMod.getLogger().warn(\"Translation API returned an unusable response: {}\", e.getMessage());\n return null;\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"DeepSeek request failed: {}\", e.getMessage(), e);\n return null;\n }\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 String detectRequestSurface(String userPrompt) {\n if (userPrompt == null || userPrompt.isBlank()) {\n return \"manager.raw\";\n }\n Matcher direct = DIRECT_SURFACE_PATTERN.matcher(userPrompt);\n if (direct.find()) {\n return direct.group(1);\n }\n Matcher mapping = MAPPING_SURFACE_PATTERN.matcher(userPrompt);\n if (mapping.find()) {\n return mapping.group(1);\n }\n return \"manager.raw\";\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 static int estimateMaxTokens(String text) {\n int length = text == null ? 0 : text.length();\n int estimated = length + 512;\n if (estimated < MIN_TRANSLATION_OUTPUT_TOKENS) {\n return MIN_TRANSLATION_OUTPUT_TOKENS;\n }\n return Math.min(estimated, MAX_TRANSLATION_OUTPUT_TOKENS);\n }\n\n private String parseResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\n }\n if (responseBody.contains(\"data:\")) {\n return parseStreamingResponse(responseBody);\n }\n return parsePlainResponse(responseBody);\n }\n\n private String parseResponse(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return null;\n }\n\n Future readerTask = 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 null;\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 String parseResponseBlocking(InputStream responseBody, long startedAt) throws IOException {\n if (responseBody == null) {\n return null;\n }\n\n StringBuilder raw = new StringBuilder();\n StringBuilder result = new StringBuilder();\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 null;\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);\n }\n } catch (RuntimeException e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return null;\n }\n\n if (sawData) {\n return ModelOutputSanitizer.sanitize(result.toString());\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 String parseStreamingResponse(String responseBody) {\n StringBuilder result = new StringBuilder();\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);\n }\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().error(\"Failed to parse streamed response\", e);\n return null;\n }\n return ModelOutputSanitizer.sanitize(result.toString());\n }\n\n private String parsePlainResponse(String responseBody) {\n if (responseBody == null || responseBody.isBlank()) {\n return null;\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 ModelOutputSanitizer.sanitize(raw);\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 TranslationMappingResult parseMappingResponse(String response, TranslationMapping mapping) {\n String sanitized = ModelOutputSanitizer.sanitize(response);\n if (sanitized == null || sanitized.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n try {\n String jsonPayload = extractJsonObjectPayload(sanitized);\n if (jsonPayload == null || jsonPayload.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n JsonObject root = JsonParser.parseString(jsonPayload).getAsJsonObject();\n String version = root.has(\"version\") ? root.get(\"version\").getAsString() : \"\";\n String jobId = root.has(\"jobId\") ? root.get(\"jobId\").getAsString() : \"\";\n if (!\"mapping-v1\".equals(version) || !mapping.taskId().equals(jobId)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n if (!root.has(\"translations\") || !root.get(\"translations\").isJsonArray()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n Map translations = new LinkedHashMap<>();\n for (var element : root.getAsJsonArray(\"translations\")) {\n if (!element.isJsonObject()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n JsonObject item = element.getAsJsonObject();\n String id = item.has(\"id\") ? item.get(\"id\").getAsString() : \"\";\n String translation = item.has(\"translation\") ? item.get(\"translation\").getAsString() : \"\";\n if (id.isBlank()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n String cleaned = ModelOutputSanitizer.sanitize(translation);\n if (cleaned == null || cleaned.isBlank() || containsDisallowedMappingTokens(cleaned)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n translations.put(id, cleaned);\n }\n\n if (translations.size() != mapping.slots().size()) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n\n for (TranslationSlot slot : mapping.slots()) {\n if (!translations.containsKey(slot.id())) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n if (slot.fixedSlot()) {\n String normalizedSource = TranslationCacheKeys.normalizeSource(slot.text());\n String normalizedTranslated = TranslationCacheKeys.normalizeSource(translations.get(slot.id()));\n if (!normalizedSource.equals(normalizedTranslated)) {\n return new TranslationMappingResult(mapping.taskId(), Map.of());\n }\n }\n }\n\n return new TranslationMappingResult(mapping.taskId(), translations);\n } catch (Exception e) {\n SimpleTranslateMod.getLogger().debug(\"Failed to parse mapping response\", e);\n return new TranslationMappingResult(mapping.taskId(), Map.of());\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 boolean containsDisallowedMappingTokens(String text) {\n if (text == null || text.isEmpty()) {\n return false;\n }\n return containsDisallowedMappingMarker(text)\n || text.contains(\"```\")\n || text.contains(\"\")\n || text.contains(\"\")\n || text.contains(\"\")\n || text.contains(\"\");\n }\n\n private boolean containsDisallowedMappingMarker(String text) {\n int index = text.indexOf(\"@@@\");\n if (index < 0) {\n return false;\n }\n Matcher matcher = ALLOWED_MAPPING_PLACEHOLDER_PATTERN.matcher(text);\n int cursor = 0;\n while (matcher.find()) {\n String between = text.substring(cursor, matcher.start());\n if (between.contains(\"@@@\")) {\n return true;\n }\n cursor = matcher.end();\n }\n return text.substring(cursor).contains(\"@@@\");\n }\n\n private void appendStreamingChunk(StringBuilder result, String json) {\n JsonObject chunk = JsonParser.parseString(json).getAsJsonObject();\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 TranslationService.ApiDetectionResult 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 = sendRequestWithProfile(profile, apiKey, model, systemPrompt, userPrompt, 64);\n if (result != null && !result.isBlank()) {\n return new TranslationService.ApiDetectionResult(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 TranslationService.ApiDetectionResult(false, \"\", \"\",\n ModConfig.normalizeApiUrl(ModConfig.DEEPSEEK_API_URL.get()),\n 0, lastMessage);\n }\n\n private TranslationService.ModelAccessResult 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 = buildSystemPrompt(\"en_us\", ModConfig.TARGET_LANGUAGE.get(), List.of(),\n RequestMode.DIRECT_FORMATTED);\n String probeUserPrompt = \"\"\"\n \n [TEXT lines=1]\n 0|Hello\n [/TEXT]\n \n \"\"\";\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.send(builder.build(),\n HttpResponse.BodyHandlers.ofInputStream());\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 TranslationService.ModelAccessResult(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 (InterruptedException e) {\n Thread.currentThread().interrupt();\n return new TranslationService.ModelAccessResult(false, modelId, 0, \"Model verification interrupted\");\n } catch (IOException | RuntimeException e) {\n lastMessage = e.getMessage() == null ? \"Model verification failed\" : e.getMessage();\n }\n }\n\n return new TranslationService.ModelAccessResult(false, modelId, lastStatus, lastMessage);\n }\n\n private static boolean isValidModelAccessProbeResponse(String value) {\n if (value == null || value.isBlank()) {\n return false;\n }\n for (String line : value.split(\"\\\\R\")) {\n String trimmed = line.trim();\n if (trimmed.matches(\"0\\\\s*[||].+\")) {\n return true;\n }\n }\n return false;\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, false));\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 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(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 requestBody.addProperty(\"max_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n\n boolean thinkingEnabled = profile.supportsThinking() && ModConfig.DEEPSEEK_THINKING_ENABLED.get();\n if (!thinkingEnabled) {\n requestBody.addProperty(\"temperature\", 0.1);\n }\n\n if (profile.supportsThinking()) {\n JsonObject thinking = new JsonObject();\n thinking.addProperty(\"type\", thinkingEnabled ? \"enabled\" : \"disabled\");\n requestBody.add(\"thinking\", thinking);\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 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 requestBody.addProperty(\"max_output_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n requestBody.addProperty(\"temperature\", 0.1);\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 requestBody.addProperty(\"max_tokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\n requestBody.addProperty(\"temperature\", 0.1);\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 systemPrompt, String userPrompt, 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.1);\n generationConfig.addProperty(\"maxOutputTokens\", Math.max(MIN_TRANSLATION_OUTPUT_TOKENS,\n Math.min(maxTokens, MAX_TRANSLATION_OUTPUT_TOKENS)));\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 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 @Override\n public boolean isReady() {\n String apiKey = sanitizeApiKey(ModConfig.DEEPSEEK_API_KEY.get());\n return apiKey != null && !apiKey.isEmpty();\n }\n\n @Override\n public String getServiceName() {\n return SERVICE_NAME;\n }\n}\n"}}, "fabric/SimpleTranslate-Fabric-1.21.1/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"6c384e5c3a953519acda61558b4a26db9ab5e385": {"t": 1786067443.2962973, "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.18.2/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068303.8274755, "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.3/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068303.9143193, "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.2/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068304.617147, "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.20.1/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068304.6391242, "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.20.1/src/main/java/com/yourname/simpletranslate/config/ModConfig.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068304.6860163, "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/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068305.9601681, "n": 14733, "h": "f04e33dbc9fec063", "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.widget.button.Button;\nimport com.yourname.simpletranslate.vanillacompat.CycleButton;\nimport net.minecraft.client.gui.widget.TextFieldWidget;\nimport net.minecraft.client.gui.screen.Screen;\nimport net.minecraft.util.text.ITextComponent;\nimport net.minecraft.util.text.Style;\nimport net.minecraft.util.IReorderingProcessor;\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 TextFieldWidget apiUrlInput;\n private TextFieldWidget apiKeyInput;\n private TextFieldWidget 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(com.yourname.simpletranslate.core.LegacyComponentFactory.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 TextFieldWidget(this.font, 0, 0, this.contentWidth, 20,\n com.yourname.simpletranslate.core.LegacyComponentFactory.translatable(\"screen.simple_translate.model_settings.api_url\"));\n this.apiUrlInput.setMaxLength(512);\n this.apiUrlInput.setValue(this.currentApiUrl);\n this.apiUrlInput.setSuggestion(com.yourname.simpletranslate.core.LegacyComponentFactory.translatable(\"screen.simple_translate.model_settings.api_url_hint\").getString());\n withTooltip(this.apiUrlInput, \"screen.simple_translate.model_settings.api_url.tooltip\");\n addEntry(this.apiUrlInput);\n\n this.apiKeyInput = new TextFieldWidget(this.font, 0, 0, this.contentWidth, 20,\n com.yourname.simpletranslate.core.LegacyComponentFactory.translatable(\"screen.simple_translate.model_settings.api_key\"));\n this.apiKeyInput.setMaxLength(512);\n this.apiKeyInput.setValue(this.currentApiKey);\n this.apiKeyInput.setSuggestion(com.yourname.simpletranslate.core.LegacyComponentFactory.translatable(\"screen.simple_translate.model_settings.api_key_hint\").getString());\n withTooltip(this.apiKeyInput, \"screen.simple_translate.model_settings.api_key.tooltip\");\n this.apiKeyInput.setFormatter((value, pos) -> value.isEmpty()\n ? IReorderingProcessor.EMPTY\n : IReorderingProcessor.forward(\"*\".repeat(value.length()), Style.EMPTY));\n addEntry(this.apiKeyInput);\n\n this.modelInput = new TextFieldWidget(this.font, 0, 0, this.contentWidth, 20,\n com.yourname.simpletranslate.core.LegacyComponentFactory.translatable(\"screen.simple_translate.model_settings.model_id\"));\n this.modelInput.setMaxLength(256);\n this.modelInput.setValue(this.currentModelId);\n this.modelInput.setSuggestion(com.yourname.simpletranslate.core.LegacyComponentFactory.translatable(\"screen.simple_translate.model_settings.model_hint\").getString());\n withTooltip(this.modelInput, \"screen.simple_translate.model_settings.model_id.tooltip\");\n addEntry(this.modelInput);\n\n this.formatButton = CycleButton.builder(format -> com.yourname.simpletranslate.core.LegacyComponentFactory.literal(format.getDisplayName())).withInitialValue(this.currentApiFormat)\n .withValues(ApiFormat.values())\n .create(0, 0, this.contentWidth, 20,\n com.yourname.simpletranslate.core.LegacyComponentFactory.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 com.yourname.simpletranslate.core.LegacyComponentFactory.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 -> com.yourname.simpletranslate.core.LegacyComponentFactory.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 com.yourname.simpletranslate.core.LegacyComponentFactory.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 ITextComponent checkLabel = this.checking\n ? com.yourname.simpletranslate.core.LegacyComponentFactory.translatable(\"screen.simple_translate.model_settings.checking\")\n : (this.status.isBlank()\n ? com.yourname.simpletranslate.core.LegacyComponentFactory.translatable(\"screen.simple_translate.model_settings.check\")\n : com.yourname.simpletranslate.core.LegacyComponentFactory.literal(shortText(this.status)));\n this.checkButton = ButtonCompat.builder(\n checkLabel,\n button -> checkApi())\n .bounds(0, 0, this.contentWidth, 20)\n .build();\n withTooltip(this.checkButton, this.status.isBlank()\n ? com.yourname.simpletranslate.core.LegacyComponentFactory.translatable(\"screen.simple_translate.model_settings.check.tooltip\")\n : com.yourname.simpletranslate.core.LegacyComponentFactory.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 com.yourname.simpletranslate.core.LegacyComponentFactory.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/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068306.016253, "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"}}, "neoforge/SimpleTranslate-NeoForge-26.1.2/src/main/java/com/yourname/simpletranslate/gui/ModelSettingsScreen.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068306.033877, "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"}}, "neoforge/SimpleTranslate-NeoForge-1.21.1/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068306.6703491, "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.4/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068306.7174683, "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.6/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068306.7645695, "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.5/src/main/java/com/yourname/simpletranslate/transport/DeepSeekTranslationService.java": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068306.7727098, "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.2/src/main/resources/assets/simple_translate/lang/en_us.json": {"c54300bbdb977cfe075e4dc0107f7267c81e9391": {"t": 1786068409.432399, "n": 44399, "h": "b708c694b27dff23", "c": "{\n \"key.simple_translate.open_settings\": \"Open Simple Translate Settings\",\n \"key.category.simple_translate.general\": \"Simple Translate\",\n \"screen.simple_translate.settings\": \"Simple Translate Settings\",\n \"screen.simple_translate.settings.global_enabled\": \"Global Translation\",\n \"screen.simple_translate.settings.global_enabled.tooltip\": \"When enabled, every translation feature you turned on can run.\",\n \"screen.simple_translate.settings.max_in_flight_batches\": \"Max In-flight Batches\",\n \"screen.simple_translate.settings.max_in_flight_batches.tooltip\": \"Set how many translation batches run at once; the default is recommended.\",\n \"screen.simple_translate.settings.direct_batch_delay\": \"Direct Batch Delay\",\n \"screen.simple_translate.settings.direct_batch_delay.tooltip\": \"Set how long nearby text waits to merge; shorter is faster, longer uses fewer requests.\",\n \"screen.simple_translate.settings.custom_font_cjk_fix\": \"Fix CJK Custom Font Missing Glyphs\",\n \"screen.simple_translate.settings.custom_font_cjk_fix.tooltip\": \"When enabled, missing Chinese, Japanese, or Korean glyphs use the default font.\",\n \"screen.simple_translate.settings.layout_critical_hud_keep_original\": \"Force Original for Layout-Critical Actionbar\",\n \"screen.simple_translate.settings.layout_critical_hud_keep_original.tooltip\": \"When enabled, special action bars that may misalign always stay original.\",\n \"screen.simple_translate.token_monitor\": \"Token Usage Monitor\",\n \"screen.simple_translate.token_monitor.tooltip\": \"Click to view API token usage for this game session.\",\n \"screen.simple_translate.token_monitor.enable\": \"Enable Monitor\",\n \"screen.simple_translate.token_monitor.enable.tooltip\": \"When enabled, records token usage for each translation request. No data is recorded when disabled.\",\n \"screen.simple_translate.token_monitor.totals\": \"Session Totals\",\n \"screen.simple_translate.token_monitor.recent\": \"Recent Requests\",\n \"screen.simple_translate.token_monitor.disabled\": \"Monitor is disabled. Enable it to start recording.\",\n \"screen.simple_translate.token_monitor.no_data\": \"No data yet\",\n \"screen.simple_translate.token_monitor.clear\": \"Clear Records\",\n \"screen.simple_translate.token_monitor.clear.tooltip\": \"Click to clear token statistics recorded during this game session.\",\n \"screen.simple_translate.token_monitor.requests\": \"Requests: %s\",\n \"screen.simple_translate.token_monitor.prompt_tokens\": \"Prompt Tokens: %s\",\n \"screen.simple_translate.token_monitor.completion_tokens\": \"Completion Tokens: %s\",\n \"screen.simple_translate.token_monitor.total_tokens\": \"Total Tokens: %s\",\n \"screen.simple_translate.token_monitor.avg_time\": \"Avg Time: %sms\",\n \"screen.simple_translate.status.ready\": \"[Ready]\",\n \"screen.simple_translate.status.not_configured\": \"[Not Configured]\",\n \"screen.simple_translate.main.subtitle\": \"Grouped by purpose; toggles save immediately and child pages return here.\",\n \"screen.simple_translate.main.section.general\": \"Master Switch\",\n \"screen.simple_translate.main.section.access\": \"Connection (Configure First)\",\n \"screen.simple_translate.main.section.scope\": \"Translation Scope\",\n \"screen.simple_translate.main.section.prompts\": \"Prompts and Dictionary\",\n \"screen.simple_translate.main.section.operation\": \"Controls and Appearance\",\n \"screen.simple_translate.main.section.advanced\": \"Advanced (Usually Unneeded)\",\n \"screen.simple_translate.main.group.chat_reading\": \"—— Chat and Reading ——\",\n \"screen.simple_translate.main.group.items_menus\": \"—— Items and Menus ——\",\n \"screen.simple_translate.main.group.screen\": \"—— On-Screen Display ——\",\n \"screen.simple_translate.main.group.world_text\": \"—— Text in the World ——\",\n \"screen.simple_translate.main.translation_api\": \"Translation API\",\n \"screen.simple_translate.main.language\": \"Language\",\n \"screen.simple_translate.main.chat\": \"Chat\",\n \"screen.simple_translate.main.book\": \"Books\",\n \"screen.simple_translate.main.item_tooltip\": \"Item Tooltips\",\n \"screen.simple_translate.main.gui\": \"Menu Screens\",\n \"screen.simple_translate.main.advancement\": \"Advancements\",\n \"screen.simple_translate.main.hud\": \"Screen HUD\",\n \"screen.simple_translate.main.sign\": \"Signs\",\n \"screen.simple_translate.main.entity\": \"Entity Names\",\n \"screen.simple_translate.main.text_display\": \"Floating Text\",\n \"screen.simple_translate.main.reference_prompt\": \"Reference Prompt\",\n \"screen.simple_translate.main.terms\": \"Terms\",\n \"screen.simple_translate.main.blacklist\": \"Blacklist\",\n \"screen.simple_translate.main.history\": \"Translation History Reference\",\n \"screen.simple_translate.main.cache\": \"Translated Cache\",\n \"screen.simple_translate.shortcuts.hint_prefix\": \"Shortcut: \",\n \"screen.simple_translate.shortcuts.modifier.ctrl\": \"Ctrl\",\n \"screen.simple_translate.shortcuts.modifier.alt\": \"Alt\",\n \"screen.simple_translate.shortcuts.modifier.shift\": \"Shift\",\n \"screen.simple_translate.main.shortcuts\": \"Shortcuts\",\n \"screen.simple_translate.main.glow\": \"Translating Glow\",\n \"screen.simple_translate.main.speed\": \"Translation Speed\",\n \"screen.simple_translate.main.display_compatibility\": \"Display Compatibility\",\n \"screen.simple_translate.main.usage\": \"Usage Statistics\",\n \"screen.simple_translate.translation_speed\": \"Translation Speed\",\n \"screen.simple_translate.translation_speed.tooltip\": \"Adjust request concurrency and the delay used to combine nearby text.\",\n \"screen.simple_translate.translation_speed.section\": \"Request Timing\",\n \"screen.simple_translate.display_compatibility\": \"Display Compatibility\",\n \"screen.simple_translate.display_compatibility.tooltip\": \"Configure general display compatibility for resource-pack custom fonts.\",\n \"screen.simple_translate.display_compatibility.section.fonts\": \"Font Compatibility\",\n \"screen.simple_translate.display_compatibility.note\": \"Only replaces missing Chinese, Japanese, or Korean glyphs in resource-pack fonts.\",\n \"screen.simple_translate.model_settings\": \"Model Settings\",\n \"screen.simple_translate.model_settings.tooltip\": \"Click to configure the translation API, key, and model.\",\n \"screen.simple_translate.model_settings.section.connection\": \"Large Model Settings\",\n \"screen.simple_translate.model_settings.api_url\": \"API URL\",\n \"screen.simple_translate.model_settings.api_url_hint\": \"Example: https://api.deepseek.com/chat/completions\",\n \"screen.simple_translate.model_settings.api_url.tooltip\": \"Enter the provider's real API URL, not an admin page, website home page, or example.com placeholder. OpenAI-compatible gateways usually use https://host/v1.\",\n \"screen.simple_translate.model_settings.api_key\": \"API Key\",\n \"screen.simple_translate.model_settings.api_key_hint\": \"Enter API key...\",\n \"screen.simple_translate.model_settings.api_key.tooltip\": \"The provider API key used for translation requests. The field masks the saved value.\",\n \"screen.simple_translate.model_settings.model_id\": \"Model\",\n \"screen.simple_translate.model_settings.model_hint\": \"Example: deepseek-v4-flash\",\n \"screen.simple_translate.model_settings.model_id.tooltip\": \"The model id to call. If left empty, the selected API format supplies its default model.\",\n \"screen.simple_translate.model_settings.api_format\": \"API Format\",\n \"screen.simple_translate.model_settings.api_format.tooltip\": \"Select the provider protocol. This affects the request body and default model.\",\n \"screen.simple_translate.model_settings.max_parallel\": \"Max Parallel Requests\",\n \"screen.simple_translate.model_settings.max_parallel.tooltip\": \"Limits simultaneous model requests. Higher values can respond faster but may hit rate limits.\",\n \"screen.simple_translate.model_settings.check\": \"Check API\",\n \"screen.simple_translate.model_settings.local_llm_warning\": \"Note: small local models translate poorly; a 32B+ model is recommended.\",\n \"screen.simple_translate.model_settings.check.tooltip\": \"Run a real translation-protocol probe with the current URL, key, format, and model. It only passes when the model returns text the game can restore.\",\n \"screen.simple_translate.model_settings.checking\": \"Checking API...\",\n \"screen.simple_translate.model_settings.status.available\": \"Translation protocol is available\",\n \"screen.simple_translate.model_settings.status.no_key\": \"Enter an API key first\",\n \"screen.simple_translate.model_settings.status.no_model\": \"Enter a model first\",\n \"screen.simple_translate.model_settings.status.manager_missing\": \"Translation service is not initialized\",\n \"screen.simple_translate.model_settings.status.failed\": \"Check failed: %s\",\n \"screen.simple_translate.language_settings\": \"Language Settings\",\n \"screen.simple_translate.language_settings.tooltip\": \"Click to choose the source and target languages.\",\n \"screen.simple_translate.translation_profile\": \"Reference Prompt\",\n \"screen.simple_translate.translation_profile.tooltip\": \"Set the context, naming preferences, and writing style the model should reference.\",\n \"screen.simple_translate.translation_profile.scope.server\": \"Current server: %s\",\n \"screen.simple_translate.translation_profile.scope.world\": \"Current world: %s\",\n \"screen.simple_translate.translation_profile.scope.global\": \"Global default profile\",\n \"screen.simple_translate.translation_profile.description\": \"Briefly describe this place and how you want it translated.\",\n \"screen.simple_translate.translation_profile.context\": \"Server Description and Naming Preferences\",\n \"screen.simple_translate.translation_profile.context.hint\": \"Example: fantasy MMORPG; localize NPC jobs naturally; transliterate place names.\",\n \"screen.simple_translate.translation_profile.context.tooltip\": \"Enter server context, titles, and fixed names for the model to follow.\",\n \"screen.simple_translate.translation_profile.save.tooltip\": \"Save this profile for future translations.\",\n \"screen.simple_translate.translation_profile.reset\": \"Reset\",\n \"screen.simple_translate.translation_profile.reset.tooltip\": \"Clear the profile and restore defaults.\",\n \"screen.simple_translate.translation_profile.saved\": \"Saved. New translations will use this profile.\",\n \"screen.simple_translate.translation_profile.reset_done\": \"Restored the default profile.\",\n \"screen.simple_translate.language_settings.section\": \"Language\",\n \"screen.simple_translate.language_settings.source\": \"Source Language\",\n \"screen.simple_translate.language_settings.source.tooltip\": \"Choose the source language to detect. Auto is best for maps, chat, and item text that mix languages.\",\n \"screen.simple_translate.language_settings.target\": \"Target Language\",\n \"screen.simple_translate.language_settings.target.tooltip\": \"Choose the output language. Changing it resets in-flight requests and isolates caches by language.\",\n \"screen.simple_translate.language_settings.source_custom\": \"Custom Source Language\",\n \"screen.simple_translate.language_settings.source_custom.tooltip\": \"When source language is custom, enter a language code such as pt_br, it, or th.\",\n \"screen.simple_translate.language_settings.target_custom\": \"Custom Target Language\",\n \"screen.simple_translate.language_settings.target_custom.tooltip\": \"When target language is custom, enter a language code. Target cannot use auto detection.\",\n \"screen.simple_translate.language_settings.custom_hint\": \"Example: pt_br / it / th\",\n \"screen.simple_translate.language_settings.lang.auto\": \"Auto Detect\",\n \"screen.simple_translate.language_settings.lang.zh_cn\": \"Simplified Chinese\",\n \"screen.simple_translate.language_settings.lang.en\": \"English\",\n \"screen.simple_translate.language_settings.lang.ja\": \"Japanese\",\n \"screen.simple_translate.language_settings.lang.ko\": \"Korean\",\n \"screen.simple_translate.language_settings.lang.es\": \"Spanish\",\n \"screen.simple_translate.language_settings.lang.fr\": \"French\",\n \"screen.simple_translate.language_settings.lang.de\": \"German\",\n \"screen.simple_translate.language_settings.lang.ru\": \"Russian\",\n \"screen.simple_translate.language_settings.lang.custom\": \"Custom\",\n \"screen.simple_translate.thinking\": \"Model Reasoning Mode\",\n \"screen.simple_translate.thinking.tooltip\": \"When enabled, reasoning models may think before translating, but respond more slowly.\",\n \"screen.simple_translate.chat_translation\": \"Chat Translation\",\n \"screen.simple_translate.chat_translation.tooltip\": \"Click to configure chat and chat hover translation.\",\n \"screen.simple_translate.book_translation\": \"Book Translation\",\n \"screen.simple_translate.book_translation.tooltip\": \"Click to configure book text and book hover translation.\",\n \"screen.simple_translate.item_tooltip_translation\": \"Item Tooltip Translation\",\n \"screen.simple_translate.item_tooltip_translation.tooltip\": \"Click to configure item tooltip translation.\",\n \"screen.simple_translate.gui_translation\": \"Translate All Current Screen Content\",\n \"screen.simple_translate.gui_translation.tooltip\": \"Configure all translatable content on the current vanilla or mod screen.\",\n \"screen.simple_translate.gui.section\": \"Visible GUI Text\",\n \"screen.simple_translate.gui.enabled\": \"Translate all current screen content\",\n \"screen.simple_translate.gui.enabled.tooltip\": \"Translate titles, labels, buttons, descriptions, and tooltips on the current vanilla or mod screen while leaving input, player names, and technical identifiers unchanged.\",\n \"screen.simple_translate.hud_translation\": \"HUD Translation\",\n \"screen.simple_translate.hud_translation.tooltip\": \"Click to configure scoreboard, boss-bar, title, and action-bar translation.\",\n \"screen.simple_translate.sign_translation\": \"Sign Translation\",\n \"screen.simple_translate.sign_translation.tooltip\": \"Click to configure sign translation.\",\n \"screen.simple_translate.advancement_translation\": \"Advancement Translation\",\n \"screen.simple_translate.advancement_translation.tooltip\": \"Click to configure advancement translation.\",\n \"screen.simple_translate.entity_translation\": \"Entity Name Translation\",\n \"screen.simple_translate.entity_translation.tooltip\": \"Click to configure entity-name translation.\",\n \"screen.simple_translate.text_display_translation\": \"Text Display Translation\",\n \"screen.simple_translate.text_display_translation.tooltip\": \"Click to configure floating map text translation.\",\n \"screen.simple_translate.term_manager\": \"Term Manager\",\n \"screen.simple_translate.term_manager.tooltip\": \"Click to set terms that must use fixed translations.\",\n \"screen.simple_translate.cache_manager\": \"Cache Manager\",\n \"screen.simple_translate.cache_manager.tooltip\": \"Click to view, edit, and share translation caches.\",\n \"screen.simple_translate.blacklist_manager\": \"Blacklist Manager\",\n \"screen.simple_translate.blacklist_manager.tooltip\": \"Click to manage text that should not be translated.\",\n \"screen.simple_translate.blacklist.entry\": \"Blacklist Entry\",\n \"screen.simple_translate.blacklist.entry_hint\": \"Text to skip...\",\n \"screen.simple_translate.blacklist.entry.tooltip\": \"Enter exact text or a term that translation should skip.\",\n \"screen.simple_translate.blacklist.add.tooltip\": \"Add the input text to the blacklist.\",\n \"screen.simple_translate.blacklist.delete.tooltip\": \"Delete the selected blacklist entry.\",\n \"screen.simple_translate.blacklist.clear.tooltip\": \"Clear all blacklist entries.\",\n \"screen.simple_translate.blacklist.export.tooltip\": \"Export the current blacklist to the config folder.\",\n \"screen.simple_translate.blacklist.import.tooltip\": \"Import and merge a blacklist from the config folder.\",\n \"screen.simple_translate.blacklist.count\": \"Blacklist entries: %s\",\n \"screen.simple_translate.chat.section.messages\": \"Chat Messages\",\n \"screen.simple_translate.chat.section.context\": \"Context Reference\",\n \"screen.simple_translate.chat.section.hover\": \"Hover Text\",\n \"screen.simple_translate.chat.enabled\": \"Chat Message Translation\",\n \"screen.simple_translate.chat.enabled.tooltip\": \"When enabled, received chat messages can be translated.\",\n \"screen.simple_translate.chat.mode\": \"Translation Mode\",\n \"screen.simple_translate.chat.mode.tooltip\": \"Translate clicked messages or new messages automatically.\",\n \"screen.simple_translate.chat.hover_enabled\": \"Chat Hover Translation\",\n \"screen.simple_translate.chat.hover_enabled.tooltip\": \"When enabled, hover text inside chat messages can be translated.\",\n \"screen.simple_translate.chat.hover_trigger_mode\": \"Chat Hover Trigger\",\n \"screen.simple_translate.chat.hover_trigger_mode.tooltip\": \"Choose automatic translation on hover or translation by shortcut.\",\n \"screen.simple_translate.tooltip_trigger_mode.hover\": \"Mouse Hover\",\n \"screen.simple_translate.tooltip_trigger_mode.shortcut\": \"Shortcut\",\n \"screen.simple_translate.chat.context_enabled\": \"Per-Line Context\",\n \"screen.simple_translate.chat.context_enabled.tooltip\": \"When enabled, earlier chat is used to keep translations coherent.\",\n \"screen.simple_translate.chat.context_count\": \"Extra Reference History\",\n \"screen.simple_translate.chat.context_count.value\": \"Extra %s\",\n \"screen.simple_translate.chat.context_count.tooltip\": \"Set how many earlier messages each translation can reference.\",\n \"screen.simple_translate.mode.auto\": \"Auto Translate\",\n \"screen.simple_translate.mode.button\": \"Button Mode\",\n \"screen.simple_translate.book.section\": \"Book\",\n \"screen.simple_translate.book.content_enabled\": \"Book Content Translation\",\n \"screen.simple_translate.book.content_enabled.tooltip\": \"When enabled, book and lectern page text is translated.\",\n \"screen.simple_translate.book.hover_enabled\": \"Book Hover Translation\",\n \"screen.simple_translate.book.hover_enabled.tooltip\": \"When enabled, hover text in book screens is translated.\",\n \"screen.simple_translate.book.translate_bookmark\": \"Translate page\",\n \"screen.simple_translate.book.original_bookmark\": \"Show original\",\n \"screen.simple_translate.item.section\": \"Item Tooltip\",\n \"screen.simple_translate.item.enabled\": \"Item Tooltip Translation\",\n \"screen.simple_translate.item.enabled.tooltip\": \"When enabled, item tooltip text is translated.\",\n \"screen.simple_translate.item.trigger_mode\": \"Item Tooltip Trigger\",\n \"screen.simple_translate.item.trigger_mode.tooltip\": \"Choose automatic item translation on hover or translation by shortcut.\",\n \"screen.simple_translate.hud.title\": \"Title/Subtitles Translation\",\n \"screen.simple_translate.hud.title.tooltip\": \"When enabled, translate centered titles and subtitles with nearby captions for coherent wording.\",\n \"screen.simple_translate.hud.actionbar\": \"Action Bar Translation\",\n \"screen.simple_translate.hud.actionbar.tooltip\": \"When enabled, translate text above the hotbar with nearby captions for coherent wording.\",\n \"screen.simple_translate.hud.wynn.section\": \"Wynncraft Features\",\n \"screen.simple_translate.hud.wynn_overlay\": \"Wynncraft Features Master Switch\",\n \"screen.simple_translate.hud.wynn_overlay.tooltip\": \"When enabled, translates Wynncraft NPC dialogues, class-selector bars, and Wynntils overlays. When disabled, all of these stay in English and no pending animation is shown.\",\n \"screen.simple_translate.hud.section\": \"HUD Elements\",\n \"screen.simple_translate.title.section.title\": \"Titles and Action Bar\",\n \"screen.simple_translate.title.section.history\": \"Context and History\",\n \"screen.simple_translate.title.section.timing\": \"Caption Batching\",\n \"screen.simple_translate.hud.scoreboard\": \"Scoreboard Translation\",\n \"screen.simple_translate.hud.scoreboard.tooltip\": \"When enabled, translate sidebar, player-list, and below-name scoreboard text while preserving colors and formatting.\",\n \"screen.simple_translate.hud.bossbar\": \"Boss Bar Translation\",\n \"screen.simple_translate.hud.bossbar.tooltip\": \"Translate boss bar names at the top of the screen.\",\n \"screen.simple_translate.hud.title_context\": \"Use HUD Caption History Context\",\n \"screen.simple_translate.hud.title_context.tooltip\": \"When enabled, titles, subtitles, and action bars use nearby text as context.\",\n \"screen.simple_translate.hud.history_chat\": \"Show Title History in Chat\",\n \"screen.simple_translate.hud.history_chat.tooltip\": \"When enabled, translated titles, subtitles, and action bars are saved to chat history.\",\n \"screen.simple_translate.hud.caption_time_hint\": \"milliseconds\",\n \"screen.simple_translate.hud.caption_batch_interval\": \"Title LLM Request Interval\",\n \"screen.simple_translate.hud.caption_batch_interval.tooltip\": \"Set the minimum delay between caption requests; shorter updates faster.\",\n \"screen.simple_translate.hud.caption_collect_window\": \"Caption Collection Wait\",\n \"screen.simple_translate.hud.caption_collect_window.tooltip\": \"Set how long captions wait to merge; longer gives the model more context.\",\n \"screen.simple_translate.sign.section\": \"Sign\",\n \"screen.simple_translate.sign.enabled\": \"Sign Translation\",\n \"screen.simple_translate.sign.enabled.tooltip\": \"When enabled, sign text in the world is translated.\",\n \"screen.simple_translate.sign.context_mode\": \"Translation Mode\",\n \"screen.simple_translate.sign.context_mode.tooltip\": \"Auto mode translates the current targeted sign. Manual mode lets keybinds select signs and submit contextual translation.\",\n \"screen.simple_translate.sign.context_mode.auto\": \"Auto Translate\",\n \"screen.simple_translate.sign.context_mode.manual\": \"Manual Selection\",\n \"screen.simple_translate.sign.radius\": \"Auto Scan Range\",\n \"screen.simple_translate.sign.radius.tooltip\": \"Scan radius for auto sign translation. Larger values cover more nearby signs.\",\n \"screen.simple_translate.sign.manual_hint\": \"Manual mode: G starts/stops drag select, H translates.\",\n \"screen.simple_translate.radius.blocks\": \"%s blocks\",\n \"screen.simple_translate.advancement.section\": \"Advancement\",\n \"screen.simple_translate.advancement.enabled\": \"Advancement Translation\",\n \"screen.simple_translate.advancement.enabled.tooltip\": \"When enabled, advancement toasts and screens are translated.\",\n \"screen.simple_translate.entity.section\": \"Entity Name\",\n \"screen.simple_translate.entity.enabled\": \"Entity Name Translation\",\n \"screen.simple_translate.entity.enabled.tooltip\": \"When enabled, mob, armor-stand, and other entity names are translated.\",\n \"screen.simple_translate.entity.radius\": \"Translation Radius\",\n \"screen.simple_translate.entity.radius.tooltip\": \"Limit the entity-name scan distance to avoid translating faraway entities.\",\n \"screen.simple_translate.text_display.section\": \"Text Display\",\n \"screen.simple_translate.text_display.enabled\": \"Text Display Translation\",\n \"screen.simple_translate.text_display.enabled.tooltip\": \"When enabled, floating text in maps is translated.\",\n \"screen.simple_translate.text_display.radius\": \"Translation Radius\",\n \"screen.simple_translate.text_display.radius.tooltip\": \"Limit the scan distance for text display entity translation.\",\n \"screen.simple_translate.save\": \"Save\",\n \"screen.simple_translate.back\": \"Back\",\n \"screen.simple_translate.back.tooltip\": \"Return to the previous screen.\",\n \"screen.simple_translate.export\": \"Export\",\n \"screen.simple_translate.import\": \"Import\",\n \"screen.simple_translate.delete\": \"Delete\",\n \"screen.simple_translate.clear\": \"Clear All\",\n \"screen.simple_translate.cache.edit\": \"Edit\",\n \"screen.simple_translate.cache.edit.tooltip\": \"Edit the selected cached translation text. Saving refreshes the in-game cache immediately.\",\n \"screen.simple_translate.cache.enabled\": \"Enable Translation Cache\",\n \"screen.simple_translate.cache.enabled.tooltip\": \"When disabled, every translation is requested live; the cache is neither read nor written.\",\n \"screen.simple_translate.cache.edit.title\": \"Edit Cached Translation\",\n \"screen.simple_translate.cache.edit.source\": \"Source\",\n \"screen.simple_translate.cache.edit.translation\": \"JSON text nodes (one per line)\",\n \"screen.simple_translate.cache.edit.preview\": \"In-game format preview (scroll wheel)\",\n \"screen.simple_translate.cache.edit.source_missing\": \"This old cache entry did not record source text; edit from the current translation.\",\n \"screen.simple_translate.cache.edit.input.tooltip\": \"Enter one translated text node per line; keep the line count unchanged and use \\\\n for line breaks.\",\n \"screen.simple_translate.cache.edit.save.tooltip\": \"Write the edited text nodes back into the cached Component JSON and refresh the game immediately.\",\n \"screen.simple_translate.cache.edit.saved\": \"Saved to cache\",\n \"screen.simple_translate.cache.edit.error.invalid-key\": \"Invalid cache key\",\n \"screen.simple_translate.cache.edit.error.blank-translation\": \"Translation cannot be empty\",\n \"screen.simple_translate.cache.edit.error.missing-entry\": \"Cache entry does not exist\",\n \"screen.simple_translate.cache.edit.error.unsupported-format\": \"This entry is not editable Component JSON, or the text-node count changed.\",\n \"screen.simple_translate.cache.entry_edited\": \"[Edited]\",\n \"screen.simple_translate.cache.search\": \"Search Cache\",\n \"screen.simple_translate.cache.search_hint\": \"Search lane / key / source / translation\",\n \"screen.simple_translate.cache.search.tooltip\": \"Filter cache entries by lane, surface, cache key, source text, or translated text.\",\n \"screen.simple_translate.cache.stats\": \"Cached: %s / Shown: %s / Categories: %s\",\n \"screen.simple_translate.cache.header.lane\": \"Lane\",\n \"screen.simple_translate.cache.header.key\": \"Cache Key\",\n \"screen.simple_translate.cache.header.translation\": \"Translation\",\n \"screen.simple_translate.cache.no_matches\": \"No matching cache entries\",\n \"screen.simple_translate.cache.server_share.on\": \"Server Share: On\",\n \"screen.simple_translate.cache.server_share.off\": \"Server Share: Off\",\n \"screen.simple_translate.cache.server_share.enabled\": \"Server shared cache is enabled for this world/server. It will sync automatically when supported.\",\n \"screen.simple_translate.cache.server_share.disabled\": \"Server shared cache is disabled for this world/server.\",\n \"screen.simple_translate.cache.server_share.tooltip\": \"When enabled, missing translations are shared with this server without replacing local translations.\",\n \"screen.simple_translate.cache.server_share.status.unsupported\": \"Server Share: this connection does not support sharing; local cache is still used\",\n \"screen.simple_translate.cache.server_share.status.connected\": \"Server Share: connected, queued %s, snapshot %s, sent %s, received %s\",\n \"screen.simple_translate.cache.export.tooltip\": \"Export the current cache as a shareable file.\",\n \"screen.simple_translate.cache.import.tooltip\": \"Import missing translations without replacing local entries.\",\n \"screen.simple_translate.cache.export.done\": \"Exported share package: %s entries / %s lanes, file: %s. You can send it directly.\",\n \"screen.simple_translate.cache.export.failed\": \"Failed to export cache. Check the log.\",\n \"screen.simple_translate.cache.import.available\": \"Found %s importable cache source(s).\",\n \"screen.simple_translate.cache.import.choose_folder\": \"Click Import and choose a cache zip, JSON, or a folder containing cache files.\",\n \"screen.simple_translate.cache.import.cancelled\": \"Import cancelled.\",\n \"screen.simple_translate.cache.import.dialog\": \"Choose Simple Translate Cache Zip or JSON\",\n \"screen.simple_translate.cache.import.none\": \"No importable cache was found in the selected location.\",\n \"screen.simple_translate.cache.import.world_mismatch\": \"The selected cache package belongs to another world/server and was skipped.\",\n \"screen.simple_translate.cache.import.done\": \"Import done: added %s / existing %s / invalid %s / world mismatch %s / failed files %s.\",\n \"screen.simple_translate.cache.import.failed\": \"Failed to import cache. Check the log.\",\n \"screen.simple_translate.terms.term.tooltip\": \"Enter a source term that should use a fixed translation.\",\n \"screen.simple_translate.terms.translation.tooltip\": \"Enter the fixed translation for this term, or leave empty for model translation.\",\n \"screen.simple_translate.terms.add.tooltip\": \"Add the term rule from the input fields.\",\n \"screen.simple_translate.terms.auto_detect\": \"Auto-detect Terms\",\n \"screen.simple_translate.terms.auto_detect.tooltip\": \"When enabled, frequently occurring terms are detected during translation and added to the dictionary.\",\n \"screen.simple_translate.terms.auto_detect_count\": \"Detection threshold: %s\",\n \"screen.simple_translate.terms.auto_detect_count.tooltip\": \"A term is added to the dictionary automatically after appearing this many times.\",\n \"screen.simple_translate.terms.delete.tooltip\": \"Delete the selected term rule.\",\n \"screen.simple_translate.terms.export.tooltip\": \"Export the current term dictionary to the config folder.\",\n \"screen.simple_translate.terms.import.tooltip\": \"Import and merge a term dictionary from the config folder.\",\n \"screen.simple_translate.hold_original.master_enabled\": \"Hold to Show Original (Master)\",\n \"screen.simple_translate.hold_original.master_enabled.tooltip\": \"When enabled, bound keys can temporarily show original text.\",\n \"screen.simple_translate.hold_original.feature.chat\": \"Chat\",\n \"screen.simple_translate.hold_original.feature.tooltip_item\": \"Item Tooltip\",\n \"screen.simple_translate.hold_original.feature.tooltip_hover\": \"Hover Tooltip\",\n \"screen.simple_translate.hold_original.feature.book\": \"Book\",\n \"screen.simple_translate.hold_original.feature.sign\": \"Sign\",\n \"screen.simple_translate.hold_original.feature.advancement\": \"Advancement\",\n \"screen.simple_translate.hold_original.feature.entity_name\": \"Entity Name\",\n \"screen.simple_translate.hold_original.feature.text_display\": \"Text Display\",\n \"screen.simple_translate.hold_original.feature.scoreboard\": \"Scoreboard\",\n \"screen.simple_translate.hold_original.feature.bossbar\": \"Boss Bar\",\n \"screen.simple_translate.hold_original.feature.title\": \"Title/Subtitle\",\n \"screen.simple_translate.hold_original.feature.actionbar\": \"Action Bar\",\n \"screen.simple_translate.hold_original.feature.gui\": \"GUI text\",\n \"screen.simple_translate.tooltip_glow.feature\": \"Translation Glow\",\n \"screen.simple_translate.tooltip_glow.feature.tooltip\": \"Click to configure the glow shown while tooltips are translated.\",\n \"screen.simple_translate.tooltip_glow.section\": \"Pending Translation Glow\",\n \"screen.simple_translate.tooltip_glow.enabled\": \"Show Translation Glow\",\n \"screen.simple_translate.tooltip_glow.enabled.tooltip\": \"When enabled, a flowing glow appears while a tooltip is translated.\",\n \"screen.simple_translate.tooltip_glow.line_width.tooltip\": \"Adjusts the thickness of the brightest glow line.\",\n \"screen.simple_translate.tooltip_glow.spread.tooltip\": \"Adjusts how far the soft glow spreads beyond the tooltip edge.\",\n \"screen.simple_translate.tooltip_glow.speed.tooltip\": \"Sets the time for one animation cycle; a shorter cycle flows faster.\",\n \"screen.simple_translate.tooltip_glow.opacity.tooltip\": \"Adjusts overall glow visibility; a higher value is clearer.\",\n \"screen.simple_translate.tooltip_glow.theme\": \"Color Theme\",\n \"screen.simple_translate.tooltip_glow.theme.tooltip\": \"Selects the soft gradient palette used by the flowing glow.\",\n \"screen.simple_translate.tooltip_glow.theme.soft\": \"Soft\",\n \"screen.simple_translate.tooltip_glow.theme.ocean\": \"Ocean\",\n \"screen.simple_translate.tooltip_glow.theme.aurora\": \"Aurora\",\n \"screen.simple_translate.tooltip_glow.theme.sunset\": \"Sunset\",\n \"screen.simple_translate.tooltip_glow.pixels\": \"%s px\",\n \"screen.simple_translate.tooltip_glow.seconds_decimal\": \"%s s\",\n \"screen.simple_translate.tooltip_glow.percent\": \"%s%%\",\n \"screen.simple_translate.tooltip_glow.preview\": \"Live Glow Preview\",\n \"screen.simple_translate.tooltip_glow.preview.tooltip\": \"This preview updates immediately when glow settings change.\",\n \"screen.simple_translate.tooltip_glow.preview.title\": \"Tooltip Preview\",\n \"screen.simple_translate.tooltip_glow.preview.pending\": \"Translating...\",\n \"screen.simple_translate.tooltip_glow.preview.hint\": \"Change the options above to preview them live\",\n \"message.simple_translate.sign_context.manual_disabled\": \"Enable manual context mode in Sign settings first\",\n \"message.simple_translate.sign_context.no_target\": \"Look at a sign first\",\n \"message.simple_translate.sign_context.added\": \"Selected sign (%s/100)\",\n \"message.simple_translate.sign_context.already_selected\": \"This sign is already selected\",\n \"message.simple_translate.sign_context.full\": \"You can select up to 100 signs. Submit the current selection first\",\n \"message.simple_translate.sign_context.empty\": \"No signs selected\",\n \"message.simple_translate.sign_context.submitted\": \"Submitted %s signs for context translation\",\n \"message.simple_translate.sign_context.drag_started\": \"Started sign drag selection. Sweep your crosshair over signs to add them\",\n \"message.simple_translate.sign_context.drag_stopped\": \"Stopped sign drag selection\",\n \"message.simple_translate.sign_context.translating\": \"Sign context translation is already running\",\n \"message.simple_translate.sign_context.success\": \"Sign context translation complete\",\n \"message.simple_translate.sign_context.partial\": \"Some signs were translated. Unstable signs stayed selected and can be submitted again\",\n \"message.simple_translate.sign_context.failed\": \"Sign context translation failed. Selection stayed selected; submit again later\",\n \"chat.simple_translate.first_run_hint\": \"Simple Translate is ready. Press U to open settings and enter an API key to start translating\",\n \"chat.simple_translate.button.translating\": \" [Translating...]\",\n \"chat.simple_translate.button.translating.hover\": \"Translating, please wait\",\n \"chat.simple_translate.button.original\": \" [Original]\",\n \"chat.simple_translate.button.original.hover\": \"Click to show original text\",\n \"chat.simple_translate.button.translate\": \" [Translate]\",\n \"chat.simple_translate.button.translate.hover\": \"Click to translate this message\",\n \"chat.simple_translate.hud_caption.show_translation\": \" [Translation]\",\n \"chat.simple_translate.hud_caption.show_original\": \" [Original]\",\n \"chat.simple_translate.hud_caption.show_translation.hover\": \"Click to show translation\",\n \"chat.simple_translate.hud_caption.show_original.hover\": \"Click to show original text\",\n \"screen.simple_translate.hud.caption.title\": \"[Title]\",\n \"screen.simple_translate.hud.caption.subtitle\": \"[Subtitle]\",\n \"screen.simple_translate.hud.caption.actionbar\": \"[Action Bar]\",\n \"screen.simple_translate.terms.term\": \"Term\",\n \"screen.simple_translate.terms.term.hint\": \"Enter source term...\",\n \"screen.simple_translate.terms.translation.hint\": \"Enter term translation...\",\n \"screen.simple_translate.terms.delete_short\": \"Del\",\n \"screen.simple_translate.terms.empty_translation\": \"(pending)\",\n \"screen.simple_translate.terms.narration\": \"%s translates to %s\",\n \"screen.simple_translate.cache.entry_narration\": \"%s %s translates to %s\",\n \"screen.simple_translate.mode.toggle_message\": \"Translation mode: %s\",\n \"screen.simple_translate.settings.open_failed\": \"Failed to open Simple Translate settings. Check latest.log\",\n \"screen.simple_translate.status.api_error\": \"[API Error]\",\n \"screen.simple_translate.chat.section.outgoing\": \"Outgoing Chat\",\n \"screen.simple_translate.chat.outgoing_enabled\": \"Translate Sent Chat\",\n \"screen.simple_translate.chat.outgoing_enabled.tooltip\": \"When enabled, Ctrl+Enter translates and sends the current message.\",\n \"screen.simple_translate.chat.outgoing_server_language\": \"Server Chat Language\",\n \"screen.simple_translate.chat.outgoing_server_language.tooltip\": \"Choose the language used by players in server chat.\",\n \"screen.simple_translate.chat.outgoing_server_language_custom\": \"Custom Server Language\",\n \"screen.simple_translate.chat.outgoing_server_language_custom.tooltip\": \"When server chat language is custom, enter a language code such as pt_br, it, or th. Auto detect is not allowed here.\",\n \"message.simple_translate.chat_outgoing.disabled\": \"Global translation is disabled; translated chat was not sent\",\n \"message.simple_translate.chat_outgoing.not_ready\": \"Translation API is not ready; translated chat was not sent\",\n \"message.simple_translate.chat_outgoing.pending\": \"Previous outgoing chat is still translating\",\n \"message.simple_translate.chat_outgoing.translating\": \"Translating to %s before sending...\",\n \"message.simple_translate.chat_outgoing.changed\": \"Input changed, so the old translated chat was cancelled\",\n \"message.simple_translate.chat_outgoing.failed\": \"Outgoing chat translation failed; original text was not sent\",\n \"screen.simple_translate.text_context\": \"Translation History Reference\",\n \"screen.simple_translate.text_context.tooltip\": \"Choose which historical translation sources the model may reference.\",\n \"screen.simple_translate.text_context.privacy\": \"API Privacy\",\n \"screen.simple_translate.text_context.privacy_notice\": \"API privacy: selected history may be sent\",\n \"screen.simple_translate.text_context.enabled\": \"Use Smart Context\",\n \"screen.simple_translate.text_context.enabled.tooltip\": \"When enabled, the model uses past translations from this server or save for consistent wording.\",\n \"screen.simple_translate.text_context.allow_shared\": \"Allow Shared Imported Translations\",\n \"screen.simple_translate.text_context.allow_shared.tooltip\": \"Permit examples imported from shared server caches. Off by default; manually edited translations still receive priority.\",\n \"screen.simple_translate.text_context.presets\": \"Source Presets\",\n \"screen.simple_translate.text_context.preset.all\": \"Select All Sources\",\n \"screen.simple_translate.text_context.preset.all.tooltip\": \"Click to select every source so the model can reference all translation types.\",\n \"screen.simple_translate.text_context.preset.all.applied\": \"Smart context is on with every source selected.\",\n \"screen.simple_translate.text_context.preset.ui\": \"Game UI Only\",\n \"screen.simple_translate.text_context.preset.ui.tooltip\": \"Click to reference item, GUI, HUD, and world text without received or sent chat.\",\n \"screen.simple_translate.text_context.preset.ui.applied\": \"Smart context is on with game-interface sources selected.\",\n \"screen.simple_translate.text_context.preset.clear\": \"Clear Source Selection\",\n \"screen.simple_translate.text_context.preset.clear.tooltip\": \"Click to deselect every source without deleting translation caches.\",\n \"screen.simple_translate.text_context.preset.clear.applied\": \"Smart context is off and every source is cleared.\",\n \"screen.simple_translate.text_context.saved\": \"Settings saved.\",\n \"screen.simple_translate.text_context.sources\": \"Context Sources\",\n \"screen.simple_translate.text_context.received_chat\": \"Received Chat\",\n \"screen.simple_translate.text_context.received_chat.tooltip\": \"Use translations of chat messages received on the current server/save as examples.\",\n \"screen.simple_translate.text_context.sent_chat\": \"Sent Chat\",\n \"screen.simple_translate.text_context.sent_chat.tooltip\": \"Use translations of your outgoing chat on the current server/save as examples.\",\n \"screen.simple_translate.text_context.item_tooltip\": \"Item Tooltips\",\n \"screen.simple_translate.text_context.item_tooltip.tooltip\": \"Use translated item tooltip text from the current server/save as examples.\",\n \"screen.simple_translate.text_context.hover_tooltip\": \"Hover Tooltips\",\n \"screen.simple_translate.text_context.hover_tooltip.tooltip\": \"Use translated chat and UI hover text from the current server/save as examples.\",\n \"screen.simple_translate.text_context.book\": \"Books\",\n \"screen.simple_translate.text_context.book.tooltip\": \"Use translated book text from the current server/save as examples.\",\n \"screen.simple_translate.text_context.sign\": \"Signs\",\n \"screen.simple_translate.text_context.sign.tooltip\": \"Use translated sign text from the current server/save as examples.\",\n \"screen.simple_translate.text_context.hud_captions\": \"Titles, Subtitles and Action Bar\",\n \"screen.simple_translate.text_context.hud_captions.tooltip\": \"Use translated titles, subtitles and ordinary action-bar captions as examples.\",\n \"screen.simple_translate.text_context.hud_progress\": \"Scoreboard, Boss Bar and Progress\",\n \"screen.simple_translate.text_context.hud_progress.tooltip\": \"Use translated scoreboard, boss-bar and progress text from the current server/save as examples.\",\n \"screen.simple_translate.text_context.entity_name\": \"Entity Names\",\n \"screen.simple_translate.text_context.entity_name.tooltip\": \"Use translated entity names from the current server/save as examples.\",\n \"screen.simple_translate.text_context.text_display\": \"Text Displays\",\n \"screen.simple_translate.text_context.text_display.tooltip\": \"Use translated TextDisplay text from the current server/save as examples.\",\n \"screen.simple_translate.gui.mode\": \"Trigger\",\n \"screen.simple_translate.gui.mode.shortcut\": \"Shortcut\",\n \"screen.simple_translate.gui.mode.auto\": \"Automatic\",\n \"screen.simple_translate.gui.mode.tooltip\": \"Shortcut mode translates new text only after a key press; cached translations still appear immediately.\",\n \"screen.simple_translate.gui.status.collecting\": \"Collecting this screen…\",\n \"screen.simple_translate.gui.status.translating\": \"Translating this screen…\",\n \"screen.simple_translate.gui.status.complete\": \"Translated %s entries\",\n \"screen.simple_translate.gui.status.empty\": \"No translatable text on this screen\",\n \"screen.simple_translate.gui.status.failed\": \"Translation failed; press the shortcut to retry\",\n \"screen.simple_translate.gui.status.hud.collecting\": \"Collecting the current view…\",\n \"screen.simple_translate.gui.status.hud.translating\": \"Translating the current view…\",\n \"screen.simple_translate.gui.status.hud.complete\": \"Translated %s entries in the current view\",\n \"screen.simple_translate.gui.status.hud.empty\": \"No translatable text in the current view\",\n \"screen.simple_translate.gui.status.hud.failed\": \"View translation failed; press the shortcut to retry\",\n \"screen.simple_translate.gui.mods.section\": \"Mod Screens\",\n \"screen.simple_translate.gui.mods.enabled\": \"Translate Mod Screens\",\n \"screen.simple_translate.gui.mods.enabled.tooltip\": \"Translate supported mod interfaces with this page's GUI translation mode.\",\n \"screen.simple_translate.gui.mods.ftb_quests.detected\": \"FTB Quests (detected)\",\n \"screen.simple_translate.gui.mods.ftb_quests.missing\": \"FTB Quests (not installed)\",\n \"screen.simple_translate.gui.mods.ftb_quests.tooltip\": \"Translate FTB Quest chapters, tasks, objectives, rewards, and hints.\",\n \"screen.simple_translate.gui.mods.ftb_quests.missing.tooltip\": \"Install FTB Quests to translate its quest screens.\",\n \"screen.simple_translate.gui.mods.tips.detected\": \"Tips (detected)\",\n \"screen.simple_translate.gui.mods.tips.missing\": \"Tips (not installed)\",\n \"screen.simple_translate.gui.mods.tips.tooltip\": \"Translate Tips overlay titles and text.\",\n \"screen.simple_translate.gui.mods.tips.missing.tooltip\": \"Install Tips to translate its on-screen tips.\",\n \"screen.simple_translate.shortcuts\": \"Shortcuts\",\n \"screen.simple_translate.shortcuts.tooltip\": \"Set a single key or key chord for each translation action.\",\n \"screen.simple_translate.shortcuts.section.actions\": \"Action Shortcuts\",\n \"screen.simple_translate.shortcuts.section.hold_original\": \"Hold to Show Original\",\n \"screen.simple_translate.shortcuts.action.toggle_global_translation\": \"Global Translation Toggle\",\n \"screen.simple_translate.shortcuts.action.toggle_chat_mode\": \"Toggle Chat Translation Mode\",\n \"screen.simple_translate.shortcuts.action.translate_gui\": \"Translate All Current Screen Content\",\n \"screen.simple_translate.shortcuts.action.translate_tooltip\": \"Translate Current Tooltip\",\n \"screen.simple_translate.shortcuts.action.sign_select\": \"Select Sign Context\",\n \"screen.simple_translate.shortcuts.action.sign_submit\": \"Submit Sign Context\",\n \"screen.simple_translate.shortcuts.record.tooltip\": \"Click, then press a keyboard or mouse chord.\",\n \"screen.simple_translate.shortcuts.clear\": \"Clear\",\n \"screen.simple_translate.shortcuts.clear.tooltip\": \"Remove this shortcut.\",\n \"screen.simple_translate.shortcuts.press_chord\": \"Press a chord…\",\n \"screen.simple_translate.shortcuts.none\": \"Unbound\",\n \"screen.simple_translate.shortcuts.saved\": \"Shortcut saved\",\n \"screen.simple_translate.shortcuts.conflict\": \"This chord conflicts; press it again to keep it\",\n \"screen.simple_translate.global_toggle.enabled\": \"Global translation enabled\",\n \"screen.simple_translate.global_toggle.disabled\": \"Global translation disabled\"\n}\n"}}}}