Introduce a new API key authentication mechanism to support service-to-service
communication (e.g., Airflow, CI/CD) without browser-based JWT login.
- Add `APIKey` model and `APIKeyPrincipal` dependency for RBAC.
- Implement `require_api_key_or_jwt` dependency with environment scoping.
- Add admin CRUD endpoints for API key lifecycle management.
- Add API key management UI in System Settings.
- Update maintenance API to support explicit `environment_id` and API key auth.
- Add i18n support for API keys and connection settings.
- Include Python and Bash usage examples in the `examples/` directory.
- Update technical specifications and documentation.
StatsBar — переделаны огромные карточки-кнопки в компактные пилюли
(rounded-full px-3 py-1 text-sm вместо p-3 grid grid-cols-4).
DatasetList — экшн-кнопки из bg-primary в outline-стиль, карточки
плотнее (p-2.5, py-0.5), ESLint-фиксы (#each key).
Backend — linked_dashboard_count теперь заполняется в списке датасетов
через /dataset/{id}/related_objects (конкурентно, Semaphore=3, timeout=10s).
Ранее поле было только в get_dataset_detail и StatsBar показывал 0.
- Add ondelete=CASCADE/SET NULL to all environment and translate FK constraints
- Add is_multimodal to GET /api/settings/consolidated response
- Fix toggleActive to not overwrite is_multimodal with false
- New SearchableMultiSelect component for dashboard search with multi-select
- Fix validation task page: task data unwrapping, date formatting, dashboard multi-select
- Fix infinite effect loop on dashboards page via queueMicrotask guard
- 3 new Alembic migrations (merge f0e9d8c7b6a5, translate b0c1d2e3f4a5)
Backend:
- /api/validation/ CRUD for validation tasks (policies)
- Run history with 6 filters + pagination
- Alembic migrations (provider_id, policy_id)
- 28 pytest tests
Frontend:
- /validation — task list page with status filters + CRUD
- /validation/[id] — task config (Config + History tabs)
- /validation/history — run history with metrics
- Sidebar nav entry under Operations
- i18n en/ru
Playwright (ScreenshotService):
- Removed 25s hardcoded sleeps → conditional waits
- CDP fallback to full_page screenshot
- Total timeout enforcement (120s)
- Debug screenshots → temp dir with cleanup
QA fixes:
- Debug screenshot accumulation in production (tempdir + rmtree)
- Frontend API payload field name mismatch
- History issues field reference fix
- delete_runs scoped by policy_id
Belief protocol:
- @RATIONALE/@REJECTED on 27 C4+ contracts
Closes: VALIDATION-REWORK-2026
All 5 functions now have proper #region/#endregion contracts:
- _detector_cache_key [C:1] — private helper
- build_detector [C:2] — public API
- get_detector [C:2] — public API with caching
- detect_language [C:2] — public API, @RAISES for safety
- batch_detect [C:3] — public API with @RELATION edges
Clean audit: 0 warnings on _lang_detect.py
- frontend: add to reload job data on param change after goto
- frontend: set existingJob from createJob response for immediate tabs
- frontend: suppress error toasts for schedule 404 (normal state)
- frontend: conditional ScheduleConfig mount to avoid 'new' job loading
- backend: skip preview check for jobs with direct Superset datasource
- backend: add orthogonal test for datasource+no-preview success case
- Extract _fallback_cancel_request for row lock timeout recovery in orchestrator_cancel
- Add flush lock timeout fallback in cancel_run
- Set error_message when all records fail in executor
- Track insert_failed state in scheduler and frontend store
- Migrate TranslationRunProgress from local polling to centralized store
- Fix nginx resolver for Docker variable-based proxy_pass
- Add FRONTEND_HOST_PORT env var to docker-compose
- Sidebar.svelte: defer healthStore.refresh() until feature flags confirm
health_monitor is enabled; skip entirely when disabled
- health.js: remove throw error from catch block — log and return null
instead, preventing 'Uncaught (in promise)' on expected 404
- test_report_audit_immutability.py: fix mock assertions —
audit_service uses logger.reason/reflect/explore, not logger.info
- HealthStore and Sidebar now produce zero network noise when
FEATURES__HEALTH_MONITOR=false
- _compute_source_hash now accepts context_keys parameter
- source_data is filtered to only include context-relevant fields (not row_id, primary keys)
- If no context_columns configured, hash is based on source_text + dict_hash + config_hash
- This ensures identical text in different rows produces a cache hit
- Add source_hash column (SHA256 of source_text+source_data+dict_hash+config_hash) to TranslationRecord for cache dedup
- Add Alembic migration b1c2d3e4f5a6 for the new column and composite index (source_hash, status)
- Implement _compute_source_hash / _check_translation_cache in executor.py — before LLM call, check if same source+dict+config combo was already translated successfully
- Implement cache-aware routing in _process_batch: full cache hit → skip LLM, partial hit → route to LLM
- Store source_hash on all new TranslationRecord rows for future cache hits
- Strengthen dictionary prompt instructions from 'use when applicable' to 'MUST use — mandatory'
- Add _enforce_dictionary() post-processing: force-replace dictionary terms in LLM output if LLM ignored them
- Fix CRITICAL: use run.dict_snapshot_hash/config_hash (exist on TranslationRun, not TranslationJob)
- Fix MAJOR: verify cached languages cover ALL target_languages before accepting cache hit
- Fix MAJOR: prevent regex back-reference injection in dictionary enforcement (use lambda)
- Fix: add joinedload to cache lookup to avoid N+1 queries
- Fix: remove redundant single-column index (composite index is sufficient)
- Fix(frontend): parse paginated /translate/dictionaries response correctly (result.items)
- Add mask_api_key() and is_masked_or_placeholder() to llm_provider service
- Return masked keys in all provider CRUD endpoints
- Reject masked/placeholder keys in fetch_models and test_provider_config
- Show masked key with Change button in ProviderConfig.svelte edit form
- Exclude masked keys from fetch-models, test, and submit payloads on frontend
- Update semantics-core skill with clarified complexity tier rules
- Switch agent modes from subagent to all
- Add RUN_CANCELLED event logging in orchestrator when executor returns CANCELLED (cancel flag path)
- Skip TranslationLanguage creation for languages matching detected source language (no ru in stats)
- Update tests for new behavior (3 entries instead of 4, no fr entry for source-match)
- Fix clickhouse insert test mocks for .options(joinedload()) chain
- All 208 translate tests pass
- Add LITELLM = "litellm" to LLMProviderType enum (already in HEAD from prev commit)
- Add comment in LLMClient.__init__ explaining standard Bearer auth for LiteLLM
- Add regression test test_provider_type_enum_values — guard against value drift
- Add test_litellm_client_uses_default_bearer_auth — verify no extra headers
- Clean up duplicate @RELATION lines in models.py
- All 20 backend + 4 frontend tests pass
- Add _normalize_timestamp_value to key column values in orchestrator.py and executor.py before SQL generation
- Fix test mocks for .options(joinedload()) chain, explicit None attrs for mock job
- Add global translation run progress indicator (TranslationRunGlobalIndicator + store)
- Fix translate page import missing translationRunStore
- All 208 translate tests pass
- Enable response_format (json_object) for all providers including Kilo/OpenRouter
- Skip reasoning_effort for Kilo/OpenRouter (returns 400 — mandatory reasoning)
- Add structured_outputs fallback: retry once without response_format if upstream
provider (e.g. StepFun) rejects it with 'structured_outputs is not supported'
- Handle model refusal field (refusal) instead of treating as empty content
- Fix None-handling guards for message/finish_reason/content fields
- Apply same fixes to both preview.py and executor.py _call_openai_compatible
- Connect orphaned TermCorrectionPopup, BulkReplaceModal, BulkCorrectionSidebar
DeepSeek v4 Flash returns finish_reason=length when reasoning uses
all output tokens. Partial row extraction via regex recovers complete
rows from truncated JSON instead of failing with 400 error.
Also: extra_body reasoning_effort for Kilo/OpenRouter proxied providers
Added disable_reasoning toggle that works via:
1. API parameter: reasoning_effort: 'none' (DeepSeek, Qwen)
2. System prompt instruction: 'Respond directly with ONLY the JSON.
Do NOT include any reasoning, thinking, or chain-of-thought'
(works for ALL models universally)
Backend: model column, schema, preview+executor payload
Frontend: checkbox in LLM settings, i18n en+ru
New _token_budget.py calculates safe batch_size and max_tokens based on source text length, target languages, dictionary size, and model context window (64K DeepSeek v4 Flash). preview.py: auto-reduces sample_size when texts are long. executor.py: per-batch dynamic max_tokens. 12 new tests.
finish_reason=length from DeepSeek v4 Flash — model runs out of
output tokens due to reasoning_content (CoT) + multi-language (4 langs).
8192 tokens ensures complete JSON response for preview and execution.
Preview + executor now log finish_reason, message_keys, and raw
response structure when LLM returns empty content, enabling
diagnosis of DeepSeek API issues (empty content with finish_reason=length)
Preview + executor now log the raw LLM response (first 1000 chars)
when JSON parsing fails, enabling debugging of malformed LLM output.
Previously ValueErrors from JSON parse failures were silently returned
as 400 without any diagnostic data in logs.
Multi-language save fix:
- MultiSelect.svelte: selected prop changed to $bindable([])
(without $bindable, bind:selected never propagated to parent component,
so target_languages always stayed as default ['en'])
Target table column mapping (INSERT enhancement):
- target_language_column — stores language code (e.g. 'ru', 'en')
- target_source_column — stores original source text
- target_source_language_column — stores detected BCP-47 source language
- SQL generator includes these columns in INSERT when configured
- Frontend: 4 input fields under 'Target Column Mapping' section
with clear labels, placeholders, and i18n hints
Backend: model, schema, service, orchestrator
Frontend: job config page, i18n en+ru
DB: 3 new columns added to production
- Delete migrate_sqlite_to_postgres.py (one-time script, no longer needed)
- Fix @INVARIANT in mappings.py: SQLite → PostgreSQL
- Remove deprecated source_language column from TranslationJob model
+ Alembic migration 8dd0a93af539 to drop column if exists
- Remove source_language from TranslateJobResponse schema
Fixes recurring 'source_language does not exist' scheduler error
CR1: Add needs_review, language_overridden to translation_languages migration
CR2: Add per_language_metrics to translation_metric_snapshots migration
H1: BulkFindReplaceService.preview() now uses replacement_text (not find pattern)
H2: get_run_records now includes per-language data via selectinload
H3: Preview per-language approve/reject uses correct (jobId, rowId, lang) args
H4: _compute_config_hash now includes target_languages
- Auto-detection of source language per row via LLM (US6)
- Multi-target translation — one LLM call for N languages (US1-US3)
- Language-aware storage: TranslationLanguage, per-language stats
- Multilingual dictionaries with language-pair-aware filtering (US7)
- Inline correction on any run result + submit-to-dictionary (US8)
- Context-aware dictionary: auto-capture row context, usage notes,
Jaccard similarity, priority flagging in LLM prompts (US8b)
- Configurable preview sample size 1-100, cost warning at >30
- Per-language history & metrics with MetricSnapshot preservation
- 36 files, +5022/-373, all specs GRACE-Poly v2.6 compliant
Translations:
- FR-045: new-key-only execution mode — translate only rows with unseen keys
- Compare source row keys against TranslationRecord.source_data from last succeeded run
- Baseline expired (>90 days) fallback to full mode with baseline_expired event
- run_noop early return when zero new keys (skip LLM + SQL)
Scheduler reliability:
- Stale PENDING protection: runs older than 1h auto-marked FAILED, no longer block schedule
- load_schedules() reloads active translation schedules from DB on restart
- add_translation_job/remove_translation_job register/unregister with APScheduler
- execution_mode column on translation_schedules with additive DB migration
Automation view:
- GET /settings/automation/translation-schedules endpoint
- Translation schedules displayed on Automation page below validation policies
Trace propagation:
- seed_trace_id() in all background entry points:
TaskManager._run_task/_flusher_loop, SchedulerService._trigger_backup,
websocket_endpoint, IdMappingService, all standalone scripts
Migrations:
- dictionary_entries: origin_run_id, origin_row_key, origin_user_id
- translation_schedules: execution_mode
Tests:
- 3 new test modules (22 tests): core_scheduler, executor_filter, scheduler_execution+guard
- Fix pre-existing translate test isolation (conftest.py)
- Fix test_list_runs_filter_status parameter
- Fix ModuleNotFoundError: 4 lazy imports in _schedule_routes.py changed
from 3-dot to 4-dot relative imports (src.api.dependencies -> src.dependencies)
- Fix TypeError: update_schedule() param name mismatch (timezone -> timezone_str)
- Add add_translation_job/remove_translation_job to SchedulerService
to register translation schedules with APScheduler via execute_scheduled_translation
- Add GET /settings/automation/translation-schedules endpoint
returning all translation schedules with joined job names
- Update Automation settings page to display translation schedules
(cron, timezone, active badge, last run) below validation policies