diff --git a/backend/src/models/translate.py b/backend/src/models/translate.py index b58856159..92cd33d41 100644 --- a/backend/src/models/translate.py +++ b/backend/src/models/translate.py @@ -52,6 +52,7 @@ class TranslationJob(Base): target_languages = Column(JSON, nullable=True, comment="List of BCP-47 target language codes (multi-language support)") provider_id = Column(String, nullable=True, comment="LLM provider ID") batch_size = Column(Integer, nullable=False, default=50, comment="Records per batch") + disable_reasoning = Column(Boolean, default=False, comment="If true, pass reasoning_effort:none to LLM to save output tokens") upsert_strategy = Column(String, nullable=False, default="MERGE", comment="MERGE, INSERT, UPDATE") # Environment association diff --git a/backend/src/plugins/translate/executor.py b/backend/src/plugins/translate/executor.py index 6fe7b630f..e681798d7 100644 --- a/backend/src/plugins/translate/executor.py +++ b/backend/src/plugins/translate/executor.py @@ -927,15 +927,19 @@ class TranslationExecutor: model = provider.default_model or "gpt-4o-mini" provider_type = provider.provider_type.lower() if provider.provider_type else "openai" + disable_reasoning = getattr(job, 'disable_reasoning', False) + if provider_type in ("openai", "openai_compatible", "openrouter", "kilo"): - return self._call_openai_compatible( + response_text = self._call_openai_compatible( base_url=provider.base_url, api_key=api_key, model=model, prompt=prompt, provider_type=provider_type, max_tokens=max_tokens, + disable_reasoning=disable_reasoning, ) + return response_text else: raise ValueError(f"Unsupported provider type '{provider_type}'") # endregion _call_llm @@ -953,6 +957,7 @@ class TranslationExecutor: prompt: str, provider_type: str = "openai", max_tokens: int = 8192, + disable_reasoning: bool = False, ) -> str: with belief_scope("TranslationExecutor._call_openai_compatible"): import requests as http_requests @@ -976,6 +981,13 @@ class TranslationExecutor: if provider_type in ("openai", "openai_compatible"): payload["response_format"] = {"type": "json_object"} + # Suppress Chain of Thought reasoning to save output tokens + # Works universally via system prompt + API parameter for models that support it + if disable_reasoning: + payload["reasoning_effort"] = "none" + # Universal instruction — all models understand "respond directly without reasoning" + payload["messages"][0] = {"role": "system", "content": "You are a database content translation assistant. Translate the provided text accurately, preserving data semantics. Respond directly with ONLY the JSON result. Do NOT include any reasoning, thinking, chain-of-thought, analysis, or explanation. Output ONLY valid JSON."} + logger.reason( f"LLM request model={payload.get('model')} " f"provider_type={provider_type} " diff --git a/backend/src/plugins/translate/preview.py b/backend/src/plugins/translate/preview.py index 4e1426de0..96c22e5b5 100644 --- a/backend/src/plugins/translate/preview.py +++ b/backend/src/plugins/translate/preview.py @@ -956,6 +956,8 @@ class TranslationPreview: model = provider.default_model or "gpt-4o-mini" provider_type = provider.provider_type.lower() if provider.provider_type else "openai" + disable_reasoning = getattr(job, 'disable_reasoning', False) + if provider_type in ("openai", "openai_compatible", "openrouter", "kilo"): response_text = self._call_openai_compatible( base_url=provider.base_url, @@ -964,6 +966,7 @@ class TranslationPreview: prompt=prompt, provider_type=provider_type, max_tokens=max_tokens, + disable_reasoning=disable_reasoning, ) else: raise ValueError(f"Unsupported provider type '{provider_type}' for preview") @@ -989,6 +992,7 @@ class TranslationPreview: prompt: str, provider_type: str = "openai", max_tokens: int = 8192, + disable_reasoning: bool = False, ) -> str: with belief_scope("TranslationPreview._call_openai_compatible"): import requests as http_requests @@ -1012,6 +1016,14 @@ class TranslationPreview: if provider_type in ("openai", "openai_compatible"): payload["response_format"] = {"type": "json_object"} + # Suppress Chain of Thought reasoning to save output tokens + # Works universally via system prompt + API parameter for models that support it + if disable_reasoning: + payload["reasoning_effort"] = "none" + # Universal instruction — all models understand "respond directly without reasoning" + system_content = "You are a database content translation assistant. Translate the provided text accurately, preserving data semantics. Respond directly with ONLY the JSON result. Do NOT include any reasoning, thinking, chain-of-thought, analysis, or explanation. Output ONLY valid JSON." + payload["messages"][0] = {"role": "system", "content": system_content} + logger.reason( f"LLM request model={payload.get('model')} " f"provider_type={provider_type} " diff --git a/backend/src/schemas/translate.py b/backend/src/schemas/translate.py index f969356b2..31338fdfc 100644 --- a/backend/src/schemas/translate.py +++ b/backend/src/schemas/translate.py @@ -52,6 +52,7 @@ class TranslateJobCreate(BaseModel): _validate_target_languages = field_validator("target_languages")(_validate_bcp47_list) batch_size: int = Field(50, description="Records per batch") + disable_reasoning: bool = Field(False, description="If true, pass reasoning_effort:none to LLM to save output tokens") upsert_strategy: str = Field("MERGE", description="UPSERT strategy: MERGE, INSERT, UPDATE") dictionary_ids: list[str] | None = Field(default_factory=list, description="Associated terminology dictionary IDs") environment_id: str | None = Field(None, description="Superset environment ID") @@ -85,6 +86,7 @@ class TranslateJobUpdate(BaseModel): _validate_target_languages = field_validator("target_languages")(_validate_bcp47_list) batch_size: int | None = None + disable_reasoning: bool | None = None upsert_strategy: str | None = None status: str | None = None dictionary_ids: list[str] | None = None @@ -118,6 +120,7 @@ class TranslateJobResponse(BaseModel): target_languages: list[str] | None = None provider_id: str | None = None batch_size: int = 50 + disable_reasoning: bool = False upsert_strategy: str = "MERGE" status: str created_by: str | None = None diff --git a/frontend/src/lib/i18n/locales/en/translate.json b/frontend/src/lib/i18n/locales/en/translate.json index 2b7b3fc9c..ca6b38417 100644 --- a/frontend/src/lib/i18n/locales/en/translate.json +++ b/frontend/src/lib/i18n/locales/en/translate.json @@ -101,7 +101,9 @@ "target_database_hint": "Database connection used for writing translated data via SQL Lab", "loading_databases": "Loading databases...", "include_source_reference": "Include source language in translations", - "include_source_reference_hint": "The original text will be stored as a verified reference copy in its detected language" + "include_source_reference_hint": "The original text will be stored as a verified reference copy in its detected language", + "disable_reasoning": "Disable reasoning (save tokens)", + "disable_reasoning_hint": "Saves output tokens by suppressing Chain of Thought reasoning" }, "preview": { "title": "Preview", diff --git a/frontend/src/lib/i18n/locales/ru/translate.json b/frontend/src/lib/i18n/locales/ru/translate.json index ad689060d..ef61b9c89 100644 --- a/frontend/src/lib/i18n/locales/ru/translate.json +++ b/frontend/src/lib/i18n/locales/ru/translate.json @@ -101,7 +101,9 @@ "target_database_hint": "Подключение к БД для записи переведённых данных через SQL Lab", "loading_databases": "Загрузка баз данных...", "include_source_reference": "Сохранять исходный текст как эталонную копию", - "include_source_reference_hint": "Исходный текст будет сохранён как верифицированная эталонная копия на его обнаруженном языке" + "include_source_reference_hint": "Исходный текст будет сохранён как верифицированная эталонная копия на его обнаруженном языке", + "disable_reasoning": "Отключить рассуждения (экономия токенов)", + "disable_reasoning_hint": "Экономит токены на выходе, отключая цепочку рассуждений (Chain of Thought)" }, "preview": { "title": "Предпросмотр", diff --git a/frontend/src/routes/translate/[id]/+page.svelte b/frontend/src/routes/translate/[id]/+page.svelte index 31f164f96..d46c84aa2 100644 --- a/frontend/src/routes/translate/[id]/+page.svelte +++ b/frontend/src/routes/translate/[id]/+page.svelte @@ -106,6 +106,7 @@ let includeSourceReference = $state(true); let providerId = $state(''); let batchSize = $state(50); + let disableReasoning = $state(false); let upsertStrategy = $state('MERGE'); let dictionaryIds = $state([]); let status = $state('DRAFT'); @@ -304,6 +305,7 @@ targetLanguages = j.target_languages?.length > 0 ? j.target_languages : (j.target_language ? [j.target_language] : ['en']); providerId = j.provider_id || ''; batchSize = j.batch_size || 50; + disableReasoning = j.disable_reasoning || false; upsertStrategy = j.upsert_strategy || 'MERGE'; dictionaryIds = j.dictionary_ids || []; status = j.status || 'DRAFT'; @@ -558,6 +560,7 @@ target_languages: targetLanguages?.length > 0 ? targetLanguages : null, provider_id: providerId || null, batch_size: parseInt(batchSize) || 50, + disable_reasoning: disableReasoning, upsert_strategy: upsertStrategy, dictionary_ids: dictionaryIds, database_dialect: databaseDialect || sourceDialect, @@ -1124,6 +1127,23 @@ + + +