perf(translate): fix slow translation startup — CJK estimation, output budget, provider token config
Root cause: batch sizing underestimated CJK token density (1.5→1.0 chars/token) and ignored output budget as primary constraint, causing cascading finish_reason=length. Changes: - _token_budget.py: CJK_RATIO 1.5→1.0, OTHER_RATIO 2.2→1.8, safety factors 0.75/0.70 - _token_budget.py: new _compute_max_rows_by_output() — output budget is PRIMARY constraint - _batch_sizer.py: resolve_provider_config() with DB-level context_window/max_output_tokens - _batch_sizer.py: INPUT_SAFETY_FACTOR applied, max_rows_by_output used as row cap - _llm_http.py: log actual usage.prompt_tokens/.completion_tokens from provider - _llm_call.py: retry only missing rows after finish_reason=length (save partial result) - models/llm.py + schema: provider-level context_window / max_output_tokens (nullable) - services/llm_provider.py: get_provider_token_config() helper - Alembic migration: add columns to llm_providers - Svelte ProviderConfig: collapsible Advanced: Token Limits section - 12 new tests (token budget, batch sizer, provider config) - All 492 tests pass
This commit is contained in:
@@ -380,3 +380,121 @@ def test_llm_provider_config_multimodal_explicit():
|
||||
is_multimodal=True,
|
||||
)
|
||||
assert config.is_multimodal is True
|
||||
|
||||
|
||||
# endregion test_llm_provider_config_multimodal_explicit
|
||||
|
||||
# region test_llm_provider_config_context_window_default [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [LLMProviderConfig]
|
||||
# @PURPOSE: Verify LLMProviderConfig.context_window defaults to None.
|
||||
def test_llm_provider_config_context_window_default():
|
||||
"""Verify default context_window is None in schema."""
|
||||
config = LLMProviderConfig(
|
||||
provider_type=LLMProviderType.OPENAI,
|
||||
name="Default Test",
|
||||
base_url="https://api.openai.com/v1",
|
||||
api_key="sk-test",
|
||||
default_model="gpt-4",
|
||||
)
|
||||
assert config.context_window is None
|
||||
|
||||
|
||||
# endregion test_llm_provider_config_context_window_default
|
||||
|
||||
# region test_llm_provider_config_context_window_explicit [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [LLMProviderConfig]
|
||||
# @PURPOSE: Verify LLMProviderConfig accepts explicit context_window value.
|
||||
def test_llm_provider_config_context_window_explicit():
|
||||
"""Verify setting context_window explicitly works."""
|
||||
config = LLMProviderConfig(
|
||||
provider_type=LLMProviderType.OPENAI,
|
||||
name="Test",
|
||||
base_url="https://api.openai.com/v1",
|
||||
api_key="sk-test",
|
||||
default_model="gpt-4",
|
||||
context_window=128000,
|
||||
max_output_tokens=16384,
|
||||
)
|
||||
assert config.context_window == 128000
|
||||
assert config.max_output_tokens == 16384
|
||||
|
||||
|
||||
# endregion test_llm_provider_config_context_window_explicit
|
||||
|
||||
# region test_get_provider_token_config_no_provider [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [LLMProviderService]
|
||||
# @PURPOSE: Verify get_provider_token_config returns all-None when provider not found.
|
||||
def test_get_provider_token_config_no_provider():
|
||||
"""When provider_id doesn't exist, returns all values as None."""
|
||||
db = MagicMock(spec=Session)
|
||||
db.query.return_value.filter.return_value.first.return_value = None
|
||||
|
||||
service = LLMProviderService(db)
|
||||
result = service.get_provider_token_config("nonexistent-id")
|
||||
assert result == {"model": None, "context_window": None, "max_output_tokens": None}
|
||||
|
||||
|
||||
# endregion test_get_provider_token_config_no_provider
|
||||
|
||||
# region test_get_provider_token_config_with_values [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [LLMProviderService]
|
||||
# @PURPOSE: Verify get_provider_token_config returns provider token limits from DB.
|
||||
def test_get_provider_token_config_with_values():
|
||||
"""Provider with context_window and max_output_tokens returns them."""
|
||||
db = MagicMock(spec=Session)
|
||||
mock_provider = MagicMock(spec=LLMProvider)
|
||||
mock_provider.default_model = "gpt-4o-mini"
|
||||
mock_provider.context_window = 128000
|
||||
mock_provider.max_output_tokens = 16384
|
||||
db.query.return_value.filter.return_value.first.return_value = mock_provider
|
||||
|
||||
service = LLMProviderService(db)
|
||||
result = service.get_provider_token_config("provider-1")
|
||||
assert result["model"] == "gpt-4o-mini"
|
||||
assert result["context_window"] == 128000
|
||||
assert result["max_output_tokens"] == 16384
|
||||
|
||||
|
||||
# endregion test_get_provider_token_config_with_values
|
||||
|
||||
# region test_get_provider_token_config_null_limits [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [LLMProviderService]
|
||||
# @PURPOSE: Verify get_provider_token_config returns None for null DB fields.
|
||||
def test_get_provider_token_config_null_limits():
|
||||
"""Provider with NULL token limits returns None values (signal to use defaults)."""
|
||||
db = MagicMock(spec=Session)
|
||||
mock_provider = MagicMock(spec=LLMProvider)
|
||||
mock_provider.default_model = "qwen-flash"
|
||||
mock_provider.context_window = None
|
||||
mock_provider.max_output_tokens = None
|
||||
db.query.return_value.filter.return_value.first.return_value = mock_provider
|
||||
|
||||
service = LLMProviderService(db)
|
||||
result = service.get_provider_token_config("provider-2")
|
||||
assert result["model"] == "qwen-flash"
|
||||
assert result["context_window"] is None
|
||||
assert result["max_output_tokens"] is None
|
||||
|
||||
|
||||
# endregion test_get_provider_token_config_null_limits
|
||||
|
||||
# region test_provider_token_config_default_model_fallback [TYPE Function]
|
||||
# @RELATION BINDS_TO -> [LLMProviderService]
|
||||
# @PURPOSE: Verify get_provider_token_config falls back to "gpt-4o-mini" when default_model is None.
|
||||
def test_provider_token_config_default_model_fallback():
|
||||
"""Provider without explicit default_model uses 'gpt-4o-mini' fallback."""
|
||||
db = MagicMock(spec=Session)
|
||||
mock_provider = MagicMock(spec=LLMProvider)
|
||||
mock_provider.default_model = None
|
||||
mock_provider.context_window = None
|
||||
mock_provider.max_output_tokens = None
|
||||
db.query.return_value.filter.return_value.first.return_value = mock_provider
|
||||
|
||||
service = LLMProviderService(db)
|
||||
result = service.get_provider_token_config("provider-3")
|
||||
assert result["model"] == "gpt-4o-mini"
|
||||
assert result["context_window"] is None
|
||||
assert result["max_output_tokens"] is None
|
||||
|
||||
|
||||
# endregion test_provider_token_config_default_model_fallback
|
||||
|
||||
@@ -115,6 +115,8 @@ class LLMProviderService:
|
||||
is_active=config.is_active,
|
||||
is_multimodal=config.is_multimodal,
|
||||
max_images=config.max_images,
|
||||
context_window=config.context_window,
|
||||
max_output_tokens=config.max_output_tokens,
|
||||
)
|
||||
self.db.add(db_provider)
|
||||
self.db.commit()
|
||||
@@ -148,6 +150,8 @@ class LLMProviderService:
|
||||
db_provider.is_active = config.is_active
|
||||
db_provider.is_multimodal = config.is_multimodal
|
||||
db_provider.max_images = config.max_images
|
||||
db_provider.context_window = config.context_window
|
||||
db_provider.max_output_tokens = config.max_output_tokens
|
||||
|
||||
self.db.commit()
|
||||
self.db.refresh(db_provider)
|
||||
@@ -235,6 +239,24 @@ class LLMProviderService:
|
||||
|
||||
# endregion get_decrypted_api_key
|
||||
|
||||
# region get_provider_token_config [TYPE Function]
|
||||
# @PURPOSE: Returns provider token limits for batch sizing.
|
||||
# @PRE provider_id must be valid.
|
||||
# @POST Returns dict with model name, context_window, max_output_tokens.
|
||||
# Values from DB take priority; None means "use PROVIDER_DEFAULTS fallback".
|
||||
# @RATIONALE Centralised helper — both _batch_proc.py and _batch_sizer.py need
|
||||
# the same resolution logic. Avoids duplicating DB queries and defaults.
|
||||
def get_provider_token_config(self, provider_id: str) -> dict:
|
||||
provider = self.get_provider(provider_id)
|
||||
if not provider:
|
||||
return {"model": None, "context_window": None, "max_output_tokens": None}
|
||||
return {
|
||||
"model": provider.default_model or "gpt-4o-mini",
|
||||
"context_window": provider.context_window,
|
||||
"max_output_tokens": provider.max_output_tokens,
|
||||
}
|
||||
# endregion get_provider_token_config
|
||||
|
||||
|
||||
# #endregion LLMProviderService
|
||||
|
||||
|
||||
Reference in New Issue
Block a user