Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
23b88f8
[bot]: bump guessit from 3.8.0 to 4.4.0
dependabot[bot] Sep 3, 2026
f242230
[bot]: bump the mantine group in /frontend with 6 updates
dependabot[bot] Sep 3, 2026
27690af
[bot]: bump sqlalchemy from 2.0.51 to 2.0.52
dependabot[bot] Sep 3, 2026
6afc372
Fix history ownership when media lookup fails
LavX Sep 4, 2026
8a98e39
Check translation eligibility before source-score logging
LavX Sep 4, 2026
2b88e81
Show owning instance names in global search results
LavX Sep 4, 2026
51834c9
Merge pull request #411 from LavX/fix/history-owner-preservation
LavX Sep 4, 2026
13db381
Merge pull request #412 from LavX/fix/auto-translation-gate
LavX Sep 4, 2026
8aaeb48
Merge pull request #413 from LavX/fix/search-instance-labels
LavX Sep 4, 2026
1fac881
Preserve provider error categories across the worker boundary
LavX Sep 4, 2026
a27c445
Scope generated sync outputs to their owning media
LavX Sep 4, 2026
fc10d8a
Merge pull request #414 from LavX/fix/provider-worker-errors
LavX Sep 4, 2026
fad498f
fix(translator): keep partial sidecar results and stop timing out fin…
LavX Sep 5, 2026
83563d3
Match sync outputs using library filename rules
LavX Sep 5, 2026
415dd5a
Update Browserslist to the patched 4.28.7 release
LavX Sep 5, 2026
47b545f
Continue past unusable provider archive candidates
LavX Sep 5, 2026
8479729
Align the cryptography security floor and verify stored secrets
LavX Sep 5, 2026
7dbc93d
fix(translator): reject an empty completed result
LavX Sep 5, 2026
f49c9fc
Merge pull request #417 from LavX/fix/frontend-browserslist-security
LavX Sep 5, 2026
b91d7e8
Merge pull request #419 from LavX/fix/cryptography-security-floor
LavX Sep 5, 2026
aac71c4
fix: discover existing sync outputs across naming modes
LavX Sep 5, 2026
ab2887c
fix: continue single-subtitle search after rejected candidates
LavX Sep 5, 2026
b94ea97
fix: preserve partial OpenRouter results and subtitle destinations
LavX Sep 5, 2026
d0a952a
fix: align startup checks with installation requirements
LavX Sep 5, 2026
f11dfb1
Merge pull request #415 from LavX/fix/sync-output-ownership
LavX Sep 5, 2026
2b1c461
Merge pull request #416 from LavX/fix/translate-partial
LavX Sep 5, 2026
f14e5ee
Fix PEP 440 matching for runtime requirements
LavX Sep 5, 2026
51b4cb9
Merge pull request #420 from LavX/fix/runtime-requirements-alignment
LavX Sep 5, 2026
7dcb908
fix: preserve requested archive selection context
LavX Sep 5, 2026
e82cae0
chore: point the translator submodule at v1.3.1
LavX Sep 5, 2026
baf0326
fix: preserve absolute archive episode numbering
LavX Sep 5, 2026
ebcd2bf
Merge pull request #421 from LavX/chore/translator-submodule-v1.3.1
LavX Sep 5, 2026
b81ec38
fix: preserve empty archive context and streamed member names
LavX Sep 5, 2026
728a365
fix: clear stale archive selector request context
LavX Sep 5, 2026
7566638
chore(sidecar): bump ai-subtitle-translator to v1.3.2
LavX Sep 5, 2026
a5caecd
Merge pull request #422 from LavX/chore/sidecar-v1.3.2
LavX Sep 5, 2026
6eb4d4a
chore(sidecar): bump ai-subtitle-translator to v1.3.3
LavX Sep 5, 2026
198aba8
fix: align GuessIt update with runtime validation
LavX Sep 6, 2026
4ac1041
build: update Mantine and pin Node 24 LTS
LavX Sep 6, 2026
2d908b0
fix: align SQLAlchemy update with runtime validation
LavX Sep 6, 2026
65985fe
fix: keep nested subtitle drops in one upload dialog
LavX Sep 6, 2026
4e85792
fix: forward archive selector episode context
LavX Sep 6, 2026
e54370b
feat(translator): provider routing setting for OpenRouter
LavX Sep 6, 2026
7a25d1a
Merge GuessIt dependency alignment
LavX Sep 6, 2026
ccbccc3
Merge Mantine update and Node 24 build pin
LavX Sep 6, 2026
1391171
Merge SQLAlchemy dependency alignment
LavX Sep 6, 2026
b0637ea
Merge provider archive candidate handling
LavX Sep 6, 2026
eb66d8b
Merge nested upload dialog fix
LavX Sep 6, 2026
24da866
fix(translator): send older sidecars the plain sort behind nitro, flo…
LavX Sep 6, 2026
4651bc0
fix(translator): let a typed :floor or :nitro model win over the rout…
LavX Sep 6, 2026
9f26fa3
chore(sidecar): bump ai-subtitle-translator to v1.3.4
LavX Sep 6, 2026
4457cb2
Merge pull request #423 from LavX/chore/sidecar-v1.3.3
LavX Sep 6, 2026
d0364a2
Merge pull request #425 from LavX/feat/openrouter-provider-routing
LavX Sep 6, 2026
c9b8224
feat(translator): adopt a routing shortcut typed into the model id
LavX Sep 6, 2026
ebe6298
fix(translator): adopt the routing shortcut when the model field is left
LavX Sep 6, 2026
eb971e5
fix(translator): close the gaps around adopting a routing shortcut
LavX Sep 6, 2026
baf1db4
fix(settings): commit the focused field on every save path, not just …
LavX Sep 6, 2026
74754bd
Merge pull request #426 from LavX/feat/routing-suffix-adoption
LavX Sep 6, 2026
4613307
fix(upload): show saved subtitles before background synchronization
LavX Sep 6, 2026
62c5f38
fix(translate): name the media in translation job labels for movies
LavX Sep 6, 2026
43ff351
fix(translate): scope media lookups for job labels to the owning inst…
LavX Sep 7, 2026
5b99da6
Merge pull request #427 from LavX/fix/translate-job-media-name
LavX Sep 7, 2026
6369405
Merge pull request #428 from LavX/fix/upload-background-sync-reviewed…
LavX Sep 7, 2026
bef7a43
Fix subtitle races during background sync and cancellation
LavX Sep 7, 2026
f5830fa
Keep translator status cards visible without counts
LavX Sep 7, 2026
0487330
release: Bazarr+ v2.6.2 (Clockwork)
LavX Sep 7, 2026
9e6354f
Document the final v2.6.2 correction pull request
LavX Sep 7, 2026
472e72d
Fix subtitle scan ownership and publication side effects
LavX Sep 7, 2026
83b59cf
Merge LavX/bazarr#429: prepare Bazarr+ v2.6.2
LavX Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ jobs:
tests/bazarr/test_rar_extraction_production_config.py \
tests/bazarr/test_provider_hub_auto_install_optin.py \
tests/bazarr/test_provider_hub_archive_select.py \
tests/bazarr/test_provider_hub_archive_candidates.py \
tests/bazarr/test_archive_member_matching.py \
tests/bazarr/test_provider_hub_worker_timeout.py \
tests/bazarr/test_arr_instances_schema.py \
Expand All @@ -203,6 +204,7 @@ jobs:
tests/bazarr/test_processing_sync_substep.py \
tests/bazarr/test_mass_download_path_mapping.py \
tests/bazarr/test_processing_postprocessing.py \
tests/bazarr/test_upload_background_sync.py \
tests/bazarr/test_sync_progress_report.py \
tests/bazarr/test_subsync_engines.py \
tests/bazarr/test_autosubsync_diagnostics.py \
Expand Down Expand Up @@ -276,6 +278,11 @@ jobs:
tests/bazarr/test_jellyfin_client.py \
tests/bazarr/test_jellyfin_operations.py \
tests/bazarr/test_logging_filters.py \
tests/bazarr/test_openrouter_provider_routing.py \
tests/bazarr/test_openrouter_model_migration.py \
tests/bazarr/test_openrouter_translator_partial.py \
tests/bazarr/test_openrouter_translator.py \
tests/bazarr/test_translator_model_migration.py \
tests/bazarr/test_provider_hub_install_serialization.py \
tests/bazarr/test_provider_hub_language_fallback.py \
tests/bazarr/test_provider_hub_release_scoring.py \
Expand All @@ -289,12 +296,14 @@ jobs:
tests/bazarr/test_sonarr_sync_episodes.py \
tests/bazarr/test_sql_profiler.py \
tests/bazarr/test_subtitle_content_sync_outputs.py \
tests/bazarr/test_subtitle_destination_path.py \
tests/bazarr/test_subtitle_download_api.py \
tests/bazarr/test_subtitles_pool.py \
tests/bazarr/test_swagger_static.py \
tests/bazarr/test_supervisor_proxy.py \
tests/bazarr/test_supervisor_stages.py \
tests/bazarr/test_tracemalloc_dumper.py \
tests/bazarr/test_translate_job_media_name.py \
tests/bazarr/test_translator_auth.py \
tests/bazarr/test_upgrade_embedded_history.py \
tests/subliminal_patch/test_animetosho.py \
Expand Down Expand Up @@ -328,6 +337,7 @@ jobs:
tests/bazarr/test_arr_pg_cutover_migration.py \
tests/bazarr/test_editor_instance_scope.py \
tests/bazarr/test_history_blacklist_scope.py \
tests/bazarr/test_history_owner_resolution.py \
tests/bazarr/test_local_id_actions.py \
tests/bazarr/test_navigation_local_ids.py \
tests/bazarr/test_provider_blacklist_instance_scope.py \
Expand All @@ -350,6 +360,7 @@ jobs:
tests/bazarr/test_indexer_owner_scope.py \
tests/bazarr/test_indexer_path_mapping_owner.py \
tests/bazarr/test_provider_hub_worker_reaper.py \
tests/bazarr/test_provider_hub_worker_errors.py \
tests/bazarr/test_job_module_identity.py \
tests/bazarr/test_sonarr_sync_memory.py \
tests/bazarr/test_anilist_index.py \
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ Bazarr+ solves it with a **Provider Priority toggle** in Settings > Providers. W
Upstream has Google Translate, Gemini, and Lingarr. Bazarr+ adds **OpenRouter** as a fourth translator engine, giving access to 300+ LLMs (Claude, Gemini, GPT, LLaMA, Grok, and more) plus any custom model ID from openrouter.ai. It runs as a separate microservice with an async job queue supporting 1-5 concurrent jobs and 1-8 parallel batches. Features include:
- **Translate from the subtitle action menu**: click (...) on a missing subtitle row, pick an existing source subtitle to translate from
- **Batch translation** for entire series/movie libraries from the Wanted pages
- **Dedicated settings page** with 4 zones: engine picker, connection config, model tuning (temperature, reasoning mode, parallel batches), and a live status panel showing queue stats, job progress, token usage, cost, and speed
- **Dedicated settings page** with 4 zones: engine picker, connection config, model tuning (temperature, reasoning mode, provider routing, parallel batches), and a live status panel showing queue stats, job progress, token usage, cost, and speed
- **Model details** fetched live from the OpenRouter API with per-million token pricing, per-episode/movie cost estimates, context length, and prompt caching indicators
- **AES-256-GCM encryption** for API keys in transit between Bazarr and the translator service, with a Test Connection button that validates encryption and API key status before saving
- **Auto disk scan** triggers Sonarr/Radarr to rescan after translation completes
Expand Down Expand Up @@ -455,7 +455,7 @@ The OpenSubtitles.org plugin no longer uses environment variables. Configure its
1. Go to **Settings** > **AI Translator**
2. Select **"AI Subtitle Translator"** as the translator engine
3. Enter your **OpenRouter API Key** (get one at [openrouter.ai/keys](https://openrouter.ai/keys))
4. Choose your preferred **AI Model** (Google: Gemini 2.5 Flash Lite Preview 09-2025 recommended)
4. Choose your preferred **AI Model** (`google/gemini-2.5-flash-lite` recommended)
5. Save and test with a manual translation

</details>
Expand Down
2 changes: 1 addition & 1 deletion ai-subtitle-translator
Submodule ai-subtitle-translator updated 51 files
+14 −0 .dockerignore
+2 −0 .gitignore
+6 −2 Dockerfile
+265 −70 README.md
+6 −1 docker-compose.yml
+193 −0 docs/BAZARR-SETUP.md
+818 −0 docs/superpowers/plans/2026-03-22-adaptive-batch-sizing.md
+264 −0 docs/superpowers/specs/2026-03-22-adaptive-batch-sizing-design.md
+72 −0 docs/superpowers/specs/2026-03-23-enriched-job-status-design.md
+107 −0 install.sh
+14 −2 pyproject.toml
+4 −1 requirements.txt
+3 −0 src/subtitle_translator/__init__.py
+252 −122 src/subtitle_translator/api/models.py
+349 −124 src/subtitle_translator/api/routes.py
+49 −0 src/subtitle_translator/cli.py
+52 −21 src/subtitle_translator/config.py
+247 −145 src/subtitle_translator/core/batch_processor.py
+41 −9 src/subtitle_translator/core/batch_sizing.py
+9 −15 src/subtitle_translator/core/srt_parser.py
+24 −29 src/subtitle_translator/core/translator.py
+130 −0 src/subtitle_translator/crypto.py
+73 −16 src/subtitle_translator/main.py
+36 −26 src/subtitle_translator/providers/base.py
+344 −142 src/subtitle_translator/providers/openrouter.py
+5 −2 src/subtitle_translator/queue/__init__.py
+283 −128 src/subtitle_translator/queue/job_manager.py
+302 −0 src/subtitle_translator/queue/job_store.py
+162 −102 src/subtitle_translator/queue/worker.py
+152 −113 tests/battle_royale_50_lines.py
+40 −39 tests/test_api.py
+317 −22 tests/test_batch_sizing.py
+91 −0 tests/test_cli.py
+246 −0 tests/test_config.py
+110 −0 tests/test_crypto.py
+225 −0 tests/test_crypto_base.py
+644 −0 tests/test_job_manager.py
+404 −0 tests/test_job_manager_extended.py
+265 −0 tests/test_job_store.py
+507 −0 tests/test_main.py
+1,279 −0 tests/test_openrouter.py
+121 −4 tests/test_parse_translations.py
+751 −0 tests/test_parser_batch_extended.py
+180 −0 tests/test_persistence_e2e.py
+211 −0 tests/test_provider_routing.py
+614 −0 tests/test_routes_auth.py
+753 −0 tests/test_routes_jobs.py
+55 −54 tests/test_translator.py
+484 −0 tests/test_translator_extended.py
+401 −0 tests/test_worker.py
+954 −0 tests/test_worker_extended.py
93 changes: 44 additions & 49 deletions bazarr/api/subtitles/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from subtitles.indexer.movies import store_subtitles_movie
from subtitles.indexer.series import store_subtitles
from subtitles.processing import ProcessSubtitlesResult
from subtitles.tools.subsync_engines import subtitle_mutation, subtitle_write_locks
from utilities.helper import get_target_folder
from utilities.path_mappings import path_mappings

Expand Down Expand Up @@ -813,13 +814,6 @@ def _save_subtitle_content(media_type, media_id, language_code, arr_instance_id=

subtitle_path, metadata = result

# Optimistic locking via ETag (optional but recommended)
if_match = request.headers.get('If-Match')
if if_match:
current_etag = generate_etag(subtitle_path)
if if_match.strip('"') != current_etag:
return 'Subtitle file has been modified since last read', 412

data = request.get_json()
if not data or 'content' not in data:
return 'Request body must include "content" field', 400
Expand All @@ -838,8 +832,19 @@ def _save_subtitle_content(media_type, media_id, language_code, arr_instance_id=
if len(encoded) > MAX_FILE_SIZE:
return f'Content too large ({len(encoded)} bytes, max {MAX_FILE_SIZE})', 413

video_path = path_mappings.path_replace_instance(
metadata['mediaPath'], metadata.get('arrInstanceId', arr_instance_id), media_type)
try:
_write_bytes_atomically(subtitle_path, encoded)
with subtitle_write_locks(video_path, subtitle_path):
if not os.path.isfile(subtitle_path):
return 'Subtitle file or directory not found', 404
if_match = request.headers.get('If-Match')
if if_match and if_match.strip('"') != generate_etag(subtitle_path):
return 'Subtitle file has been modified since last read', 412
with subtitle_mutation(video_path, subtitle_path):
_write_bytes_atomically(subtitle_path, encoded)
_apply_subtitle_chmod(subtitle_path)
new_etag = generate_etag(subtitle_path)
except FileNotFoundError:
return 'Subtitle file or directory not found', 404
except PermissionError:
Expand All @@ -849,11 +854,7 @@ def _save_subtitle_content(media_type, media_id, language_code, arr_instance_id=
return 'No space left on device', 507
raise

_apply_subtitle_chmod(subtitle_path)

_refresh_media_subtitles(media_type, media_id, metadata)

new_etag = generate_etag(subtitle_path)
response = make_response('', 204)
response.headers['ETag'] = f'"{new_etag}"'
return response
Expand Down Expand Up @@ -900,15 +901,20 @@ def promote_sync_subtitle(media_type, media_id, target_language, source_language
if os.path.realpath(source_path) == os.path.realpath(target_path):
return 'Source and target subtitles are the same file', 400

video_path = path_mappings.path_replace_instance(
metadata['mediaPath'], metadata.get('arrInstanceId', arr_instance_id), media_type)
try:
file_size = os.path.getsize(source_path)
if file_size > MAX_FILE_SIZE:
return f'Subtitle file too large ({file_size} bytes, max {MAX_FILE_SIZE})', 413

with open(source_path, 'rb') as source_file:
data = source_file.read()

_write_bytes_atomically(target_path, data)
with subtitle_write_locks(video_path, source_path, target_path):
file_size = os.path.getsize(source_path)
if file_size > MAX_FILE_SIZE:
return f'Subtitle file too large ({file_size} bytes, max {MAX_FILE_SIZE})', 413
if not os.path.isfile(target_path):
return 'Subtitle file or directory not found', 404
with open(source_path, 'rb') as source_file:
data = source_file.read()
with subtitle_mutation(video_path, target_path):
_write_bytes_atomically(target_path, data)
_apply_subtitle_chmod(target_path)
except FileNotFoundError:
return 'Subtitle file or directory not found', 404
except PermissionError:
Expand All @@ -918,16 +924,17 @@ def promote_sync_subtitle(media_type, media_id, target_language, source_language
return 'No space left on device', 507
raise

_apply_subtitle_chmod(target_path)
_log_promoted_sync_history(
media_type=media_type,
media_id=media_id,
target_language=target_language,
source_language=source_language,
target_path=target_path,
metadata=metadata,
)
_refresh_media_subtitles(media_type, media_id, metadata)
try:
_refresh_media_subtitles(media_type, media_id, metadata)
finally:
_log_promoted_sync_history(
media_type=media_type,
media_id=media_id,
target_language=target_language,
source_language=source_language,
target_path=target_path,
metadata=metadata,
)

return {
'sourceLanguage': source_language,
Expand Down Expand Up @@ -1096,24 +1103,20 @@ def _create_subtitle(media_type, media_id, arr_instance_id=None):
subtitle_path.startswith(target_folder_real + os.sep)):
return 'Invalid subtitle path', 400

# Check for existing file
if os.path.isfile(subtitle_path):
return 'Subtitle file already exists', 409

# Encode content
encoded = content.encode('utf-8')
if len(encoded) > MAX_FILE_SIZE:
return f'Content too large ({len(encoded)} bytes, max {MAX_FILE_SIZE})', 413

# Write atomically
# Check and create under the same mutation lock as replacements.
try:
fd, tmp_path = tempfile.mkstemp(dir=target_folder)
try:
os.write(fd, encoded)
finally:
os.close(fd)
with subtitle_write_locks(video_path, subtitle_path):
if os.path.isfile(subtitle_path):
return 'Subtitle file already exists', 409
with subtitle_mutation(video_path, subtitle_path):
_write_bytes_atomically(subtitle_path, encoded)
_apply_subtitle_chmod(subtitle_path)

os.replace(tmp_path, subtitle_path)
except FileNotFoundError:
return 'Target directory not found', 404
except PermissionError:
Expand All @@ -1123,14 +1126,6 @@ def _create_subtitle(media_type, media_id, arr_instance_id=None):
return 'No space left on device', 507
raise

# Apply chmod if configured
if settings.general.chmod_enabled:
try:
chmod_value = int(settings.general.chmod, 8)
os.chmod(subtitle_path, chmod_value)
except Exception:
pass

# Force re-scan subtitles from disk using the media (video) path
if media_type == 'episode':
store_subtitles(row.path, video_path, use_cache=False, arr_instance_id=arr_instance_id)
Expand Down
2 changes: 2 additions & 0 deletions bazarr/api/translator/translator.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from app.config import settings
from app.jobs_queue import jobs_queue
from subtitles.tools.translate.services.auth import get_translator_auth_headers
from subtitles.tools.translate.services.openrouter_translator import build_provider_config
from ..utils import authenticate

api_ns_translator = Namespace('Translator', description='AI Subtitle Translator service operations')
Expand Down Expand Up @@ -124,6 +125,7 @@ def post(self):
"apiKey": api_key,
"model": settings.translator.openrouter_model,
"temperature": settings.translator.openrouter_temperature,
"provider": build_provider_config(),
}
}

Expand Down
29 changes: 28 additions & 1 deletion bazarr/app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,17 @@ def check_parser_binary(value):
Validator('translator.lingarr_url', must_exist=True, default='http://lingarr:9876', is_type_of=str),
Validator('translator.openrouter_url', must_exist=True, default='http://subtitle-translator:8765', is_type_of=str),
Validator('translator.openrouter_api_key', must_exist=True, default='', is_type_of=str, cast=str),
Validator('translator.openrouter_model', must_exist=True, default='google/gemini-2.5-flash-preview-05-20', is_type_of=str),
Validator('translator.openrouter_model', must_exist=True, default='google/gemini-2.5-flash-lite', is_type_of=str),
Validator('translator.openrouter_temperature', must_exist=True, default=0.3, is_type_of=float),
Validator('translator.openrouter_max_concurrent', must_exist=True, default=2, is_type_of=int, gte=1, lte=10),
Validator('translator.openrouter_reasoning', must_exist=True, default='disabled', is_type_of=str,
is_in=['disabled', 'low', 'medium', 'high']),
Validator('translator.openrouter_parallel_batches', must_exist=True, default=4, is_type_of=int, gte=1, lte=8),
# Which OpenRouter provider serves the model: throughput (the sidecar's historical default),
# nitro/floor (OpenRouter's slug shortcuts, which also unlock the priority/flex tiers),
# price, latency, or OpenRouter's own load balancing.
Validator('translator.openrouter_provider_routing', must_exist=True, default='throughput', is_type_of=str,
is_in=['throughput', 'nitro', 'price', 'floor', 'latency', 'default']),
Validator('translator.openrouter_encryption_key', must_exist=True, default='', is_type_of=str, cast=str),
Validator('translator.lingarr_token', must_exist=True, default='', is_type_of=str, cast=str),

Expand Down Expand Up @@ -727,6 +732,26 @@ def write_config():
_force_first_save_migration = False


# OpenRouter retired these ids, including Bazarr's default and documented recommendation.
# Users who never chose a model would otherwise get a model-not-found error.
RETIRED_OPENROUTER_MODELS = {
'google/gemini-2.5-flash-preview-05-20': 'google/gemini-2.5-flash',
'google/gemini-2.5-flash-lite-preview-06-17': 'google/gemini-2.5-flash-lite',
'google/gemini-2.5-flash-lite-preview-09-2025': 'google/gemini-2.5-flash-lite',
'google/gemini-3-pro-preview': 'google/gemini-3.1-pro-preview',
}


def migrate_retired_openrouter_model(settings) -> bool:
current_model = settings.translator.openrouter_model.strip()
replacement = RETIRED_OPENROUTER_MODELS.get(current_model)
if replacement is None:
return False
settings.translator.openrouter_model = replacement
logging.warning(f'Replaced retired OpenRouter model {current_model} with {replacement}') # noqa: G004
return True


base_url = settings.general.base_url.rstrip('/')

array_keys = ['excluded_tags',
Expand Down Expand Up @@ -790,6 +815,8 @@ def write_config():
settings.translator.gemini_keys = [legacy_key]
del settings.translator.gemini_key

migrate_retired_openrouter_model(settings)

# save updated settings to file
write_config()

Expand Down
Loading
Loading