Skip to content

Commit 0ae64d8

Browse files
committed
fix: correct pricing calculation for providers using $/token format
Problem: -------- Commit c6a90d5 added division by DEFAULT_TOKEN_PRICE_RATIO (1000000) to convert pricing from $/M to $/token format. However, this was applied to ALL providers, but different providers use different pricing formats: - Synthetic and OpenRouter: Already return pricing in $/token format (e.g., "$0.00000055" per token) - LiteLLM's model_prices_and_context_window.json: Uses $/M format (e.g., "2.5e-06" = $2.5/M tokens) This caused synthetic provider pricing to be divided by 1,000,000 twice, making costs appear 1,000,000x too small. For example: - synthetic/hf:zai-org/GLM-4.7: $0.00000055/token became $0.00000000000055/token - User reported: 17k sent, 10 received tokens showed cost of $0.0000000096 Solution: --------- Added a heuristic in ModelProviderManager._record_to_info() to detect pricing format before applying conversion: - If cost >= 0.001: Treat as $/M format, divide by 1,000,000 - If cost < 0.001: Treat as $/token format, no division This threshold (0.001) was chosen because: - $/M pricing is typically >= $0.001/M (e.g., $1.0/M, $2.5/M) - $/token pricing is typically < $0.001/token (e.g., $0.00000055/token) Changes: -------- 1. aider/helpers/model_providers.py: - Added _normalize_cost() helper function to intelligently handle both formats - Replaced unconditional division with format-aware normalization 2. tests/basic/test_model_provider_manager.py: - Added test_pricing_normalization_detects_token_format: Verifies $/token pricing is not divided - Added test_pricing_normalization_detects_million_format: Verifies $/M pricing is converted correctly Impact: ------- - Synthetic provider models now display correct pricing - Existing $/M format providers continue to work correctly - No breaking changes to API or behavior Co-authored-by: aider-ce (synthetic/hf:zai-org/GLM-4.7)
1 parent a64b563 commit 0ae64d8

2 files changed

Lines changed: 82 additions & 4 deletions

File tree

aider/helpers/model_providers.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -449,14 +449,24 @@ def _record_to_info(self, record: Dict, provider: str) -> Dict:
449449
if max_output_tokens is None:
450450
max_output_tokens = context_len
451451

452+
# Normalize pricing: detect if values are in $/M format vs $/token format
453+
# If cost >= 0.001, it's likely in $/M format (e.g., "1.0" = $1/M tokens)
454+
# If cost < 0.001, it's likely already in $/token format (e.g., "0.00000055")
455+
def _normalize_cost(cost: Optional[float]) -> float:
456+
if cost is None or cost == 0:
457+
return 0.0
458+
if cost >= 0.001:
459+
# Likely in $/M format, convert to $/token
460+
return cost / self.DEFAULT_TOKEN_PRICE_RATIO
461+
# Already in $/token format
462+
return cost
463+
452464
info = {
453465
"max_input_tokens": context_len,
454466
"max_tokens": max_tokens,
455467
"max_output_tokens": max_output_tokens,
456-
"input_cost_per_token": (
457-
input_cost or 0
458-
) / self.DEFAULT_TOKEN_PRICE_RATIO, # Might Only Apply to Chutes and Be a thing we configure per-provider
459-
"output_cost_per_token": (output_cost or 0) / self.DEFAULT_TOKEN_PRICE_RATIO,
468+
"input_cost_per_token": _normalize_cost(input_cost),
469+
"output_cost_per_token": _normalize_cost(output_cost),
460470
"litellm_provider": provider,
461471
"mode": record.get("mode", "chat"),
462472
}

tests/basic/test_model_provider_manager.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,74 @@ def _failing_fetch(*args, **kwargs):
282282
assert info["input_cost_per_token"] == 0.5 / manager.DEFAULT_TOKEN_PRICE_RATIO
283283

284284

285+
def test_pricing_normalization_detects_token_format(tmp_path):
286+
"""Test that pricing < 0.001 is treated as $/token, not $/M."""
287+
payload = {
288+
"data": [
289+
{
290+
"id": "demo/model",
291+
"context_length": 2048,
292+
# Pricing in $/token format (like synthetic provider)
293+
"pricing": {"prompt": "0.00000055", "completion": "0.00000219"},
294+
}
295+
]
296+
}
297+
298+
config = {
299+
"demo": {
300+
"api_base": "https://example.com/v1",
301+
"requires_api_key": False,
302+
}
303+
}
304+
305+
manager = _make_manager(tmp_path, config)
306+
cache_file = manager._get_cache_file("demo")
307+
cache_file.write_text(json.dumps(payload))
308+
manager._cache_loaded["demo"] = True
309+
manager._provider_cache["demo"] = payload
310+
311+
info = manager.get_model_info("demo/demo/model")
312+
313+
assert info["max_input_tokens"] == 2048
314+
# Values < 0.001 should NOT be divided (already in $/token format)
315+
assert info["input_cost_per_token"] == 0.00000055
316+
assert info["output_cost_per_token"] == 0.00000219
317+
318+
319+
def test_pricing_normalization_detects_million_format(tmp_path):
320+
"""Test that pricing >= 0.001 is treated as $/M and converted to $/token."""
321+
payload = {
322+
"data": [
323+
{
324+
"id": "demo/model",
325+
"context_length": 2048,
326+
# Pricing in $/M format (like some providers)
327+
"pricing": {"prompt": "1.0", "completion": "2.0"},
328+
}
329+
]
330+
}
331+
332+
config = {
333+
"demo": {
334+
"api_base": "https://example.com/v1",
335+
"requires_api_key": False,
336+
}
337+
}
338+
339+
manager = _make_manager(tmp_path, config)
340+
cache_file = manager._get_cache_file("demo")
341+
cache_file.write_text(json.dumps(payload))
342+
manager._cache_loaded["demo"] = True
343+
manager._provider_cache["demo"] = payload
344+
345+
info = manager.get_model_info("demo/demo/model")
346+
347+
assert info["max_input_tokens"] == 2048
348+
# Values >= 0.001 should be divided by 1000000 (convert $/M to $/token)
349+
assert info["input_cost_per_token"] == 1.0 / manager.DEFAULT_TOKEN_PRICE_RATIO
350+
assert info["output_cost_per_token"] == 2.0 / manager.DEFAULT_TOKEN_PRICE_RATIO
351+
352+
285353
def test_model_info_manager_delegates_to_provider(monkeypatch, tmp_path):
286354
monkeypatch.setattr(
287355
"aider.models.litellm",

0 commit comments

Comments
 (0)