Update
This commit is contained in:
@@ -331,6 +331,140 @@ def test_embeddings_disabled_returns_503(local_db, monkeypatch):
|
||||
assert resp.status_code == 503
|
||||
|
||||
|
||||
class FakeImageClient_openai_gateway:
|
||||
def __init__(self, *a, **k):
|
||||
self.calls = []
|
||||
|
||||
def build_request(self, method, url, headers=None, json=None, content=None):
|
||||
return FakeRequest(method, url, json)
|
||||
|
||||
async def send(self, request):
|
||||
self.calls.append((request.url, request.json_body))
|
||||
body = request.json_body or {}
|
||||
return FakeResp_openai_gateway(
|
||||
payload={
|
||||
"created": 1,
|
||||
"model": body.get("model"),
|
||||
"data": [{"b64_json": "aGVsbG8="}],
|
||||
"usage": {"cost": 0.05},
|
||||
}
|
||||
)
|
||||
|
||||
async def aclose(self):
|
||||
pass
|
||||
|
||||
|
||||
def test_images_remaps_alias_to_configured_model(local_db, monkeypatch):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeImageClient_openai_gateway)
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_force_model"] = False
|
||||
cfg["gateway_image_enabled"] = True
|
||||
cfg["gateway_image_model"] = "black-forest-labs/flux.2-pro"
|
||||
rt = svc.runtime()
|
||||
run_async(
|
||||
rt.handle_images(
|
||||
{
|
||||
"model": "molodetz-img-small",
|
||||
"prompt": "award emblem",
|
||||
"size": "512x512",
|
||||
},
|
||||
cfg,
|
||||
("guest", "test"),
|
||||
"test",
|
||||
)
|
||||
)
|
||||
assert rt._client.calls[-1][1]["model"] == "black-forest-labs/flux.2-pro"
|
||||
|
||||
|
||||
def test_image_route_overrides_upstream(local_db, monkeypatch):
|
||||
from devplacepy.services.openai_gateway import routing
|
||||
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeImageClient_openai_gateway)
|
||||
routing.provider_store.set(
|
||||
routing.ProviderIn(
|
||||
name="gwimg",
|
||||
base_url="https://routed.example/v1/chat/completions",
|
||||
api_key="img-key",
|
||||
)
|
||||
)
|
||||
routing.model_store.set(
|
||||
routing.ModelRouteIn(
|
||||
source_model="custom-img",
|
||||
provider="gwimg",
|
||||
target_model="vendor/flux-pro",
|
||||
kind="image",
|
||||
price_input_per_m=0.06,
|
||||
)
|
||||
)
|
||||
try:
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
rt = svc.runtime()
|
||||
run_async(
|
||||
rt.handle_images(
|
||||
{
|
||||
"model": "custom-img",
|
||||
"prompt": "trophy",
|
||||
"response_format": "b64_json",
|
||||
},
|
||||
cfg,
|
||||
("guest", "test"),
|
||||
"test",
|
||||
)
|
||||
)
|
||||
url, body = rt._client.calls[-1]
|
||||
assert str(url) == "https://routed.example/v1/images"
|
||||
assert body["model"] == "vendor/flux-pro"
|
||||
finally:
|
||||
routing.model_store.remove("custom-img")
|
||||
routing.provider_store.remove("gwimg")
|
||||
|
||||
|
||||
def test_images_success_records_ledger(local_db, monkeypatch):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeImageClient_openai_gateway)
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_image_enabled"] = True
|
||||
cfg["gateway_image_model"] = "black-forest-labs/flux.2-pro"
|
||||
rt = svc.runtime()
|
||||
before = rt.image_calls
|
||||
resp = run_async(
|
||||
rt.handle_images(
|
||||
{"model": "molodetz-img-small", "prompt": "badge"},
|
||||
cfg,
|
||||
("guest", "img_ledger"),
|
||||
"test",
|
||||
)
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
payload = json.loads(bytes(resp.body).decode())
|
||||
assert payload["data"][0]["b64_json"] == "aGVsbG8="
|
||||
assert rt.image_calls == before + 1
|
||||
row = get_table("gateway_usage_ledger").find_one(owner_id="img_ledger")
|
||||
assert row is not None
|
||||
assert row["backend"] == "image"
|
||||
assert row["endpoint"] == "images/generations"
|
||||
assert row["requested_model"] == "molodetz-img-small"
|
||||
assert row["model"] == "black-forest-labs/flux.2-pro"
|
||||
assert row["success"] == 1
|
||||
assert float(row["cost_usd"]) == 0.05
|
||||
|
||||
|
||||
def test_images_disabled_returns_503(local_db, monkeypatch):
|
||||
monkeypatch.setattr(gwmod.httpx, "AsyncClient", FakeImageClient_openai_gateway)
|
||||
svc = GatewayService()
|
||||
cfg = svc.effective_config()
|
||||
cfg["gateway_image_enabled"] = False
|
||||
rt = svc.runtime()
|
||||
resp = run_async(
|
||||
rt.handle_images(
|
||||
{"prompt": "badge"}, cfg, ("guest", "test"), "test"
|
||||
)
|
||||
)
|
||||
assert resp.status_code == 503
|
||||
|
||||
|
||||
def test_compute_cost_embed_branch():
|
||||
from devplacepy.services.openai_gateway.usage import (
|
||||
Pricing,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import pytest
|
||||
from pydantic import ValidationError
|
||||
|
||||
from devplacepy.services.openai_gateway import routing as r
|
||||
from devplacepy.services.openai_gateway.usage import pricing_from_cfg
|
||||
|
||||
@@ -14,6 +17,7 @@ def _cleanup(providers, models):
|
||||
def test_no_routes_pass_through(local_db):
|
||||
assert r.chat_overlay("ghost-model-xyz", {}) is None
|
||||
assert r.embed_overlay("ghost-embed-xyz", {}) is None
|
||||
assert r.image_overlay("ghost-image-xyz", {}) is None
|
||||
|
||||
|
||||
def test_chat_route_overlay_and_economy(local_db):
|
||||
@@ -80,6 +84,37 @@ def test_vision_merge_overlay(local_db):
|
||||
_cleanup(["utvis"], ["ut-vision"])
|
||||
|
||||
|
||||
def test_image_route_and_kind_isolation(local_db):
|
||||
r.provider_store.set(
|
||||
r.ProviderIn(
|
||||
name="utimg",
|
||||
base_url="https://img.example/v1/chat/completions",
|
||||
api_key="ik",
|
||||
)
|
||||
)
|
||||
r.model_store.set(
|
||||
r.ModelRouteIn(
|
||||
source_model="ut-image",
|
||||
provider="utimg",
|
||||
target_model="vendor/flux",
|
||||
kind="image",
|
||||
price_input_per_m=0.05,
|
||||
)
|
||||
)
|
||||
try:
|
||||
overlay = r.image_overlay("ut-image", {})
|
||||
assert overlay["gateway_image_model"] == "vendor/flux"
|
||||
assert overlay["gateway_image_url"] == "https://img.example/v1/images"
|
||||
assert overlay["gateway_image_key"] == "ik"
|
||||
assert overlay["gateway_image_price_per_call"] == 0.05
|
||||
assert r.chat_overlay("ut-image", {}) is None
|
||||
assert r.embed_overlay("ut-image", {}) is None
|
||||
pricing = pricing_from_cfg(overlay)
|
||||
assert pricing.image_per_call == 0.05
|
||||
finally:
|
||||
_cleanup(["utimg"], ["ut-image"])
|
||||
|
||||
|
||||
def test_embed_route_and_kind_isolation(local_db):
|
||||
r.provider_store.set(
|
||||
r.ProviderIn(
|
||||
@@ -140,3 +175,108 @@ def test_blank_provider_uses_default_upstream(local_db):
|
||||
assert overlay["gateway_price_output_per_m"] == 3.0
|
||||
finally:
|
||||
_cleanup([], ["ut-default"])
|
||||
|
||||
|
||||
def test_tier2_and_off_peak_fields_propagate_through_overlay(local_db):
|
||||
r.model_store.set(
|
||||
r.ModelRouteIn(
|
||||
source_model="ut-tiered",
|
||||
target_model="vendor/tiered",
|
||||
kind="chat",
|
||||
price_cache_hit_per_m=0.0028,
|
||||
price_cache_miss_per_m=0.14,
|
||||
price_output_per_m=0.28,
|
||||
context_tier_threshold_tokens=128_000,
|
||||
price_output_per_m_tier2=0.56,
|
||||
off_peak_start_minute=990,
|
||||
off_peak_end_minute=30,
|
||||
off_peak_discount_pct=25.0,
|
||||
)
|
||||
)
|
||||
try:
|
||||
overlay = r.chat_overlay("ut-tiered", {})
|
||||
assert overlay["gateway_context_tier_threshold_tokens"] == 128_000
|
||||
assert overlay["gateway_price_output_per_m_tier2"] == 0.56
|
||||
assert overlay["gateway_price_cache_hit_per_m_tier2"] is None
|
||||
assert overlay["gateway_off_peak_start_minute"] == 990
|
||||
assert overlay["gateway_off_peak_end_minute"] == 30
|
||||
assert overlay["gateway_off_peak_discount_pct"] == 25.0
|
||||
pricing = pricing_from_cfg(overlay)
|
||||
assert pricing.context_tier_threshold_tokens == 128_000
|
||||
assert pricing.chat_output_per_m_tier2 == 0.56
|
||||
assert pricing.chat_cache_hit_per_m_tier2 is None
|
||||
assert pricing.off_peak_start_minute == 990
|
||||
finally:
|
||||
_cleanup([], ["ut-tiered"])
|
||||
|
||||
|
||||
def test_unrouted_request_never_gains_tier_or_off_peak_keys(local_db):
|
||||
assert r.chat_overlay("ghost-model-untiered", {}) is None
|
||||
pricing = pricing_from_cfg({})
|
||||
assert pricing.context_tier_threshold_tokens == 0
|
||||
assert pricing.off_peak_start_minute is None
|
||||
assert pricing.off_peak_end_minute is None
|
||||
assert pricing.chat_output_per_m_tier2 is None
|
||||
|
||||
|
||||
def test_off_peak_window_requires_both_start_and_end():
|
||||
with pytest.raises(ValidationError):
|
||||
r.ModelRouteIn(
|
||||
source_model="ut-bad-window",
|
||||
target_model="vendor/x",
|
||||
off_peak_start_minute=60,
|
||||
)
|
||||
with pytest.raises(ValidationError):
|
||||
r.ModelRouteIn(
|
||||
source_model="ut-bad-window",
|
||||
target_model="vendor/x",
|
||||
off_peak_end_minute=120,
|
||||
)
|
||||
|
||||
|
||||
def test_seed_default_image_routes_is_idempotent(local_db):
|
||||
r.model_store.remove(r.IMAGE_SOURCE_MODEL)
|
||||
r.seed_default_image_routes()
|
||||
route = r.model_store.get(r.IMAGE_SOURCE_MODEL)
|
||||
assert route is not None
|
||||
assert route.kind == "image"
|
||||
assert route.target_model == r.FLUX_TARGET_MODEL
|
||||
r.seed_default_image_routes()
|
||||
assert r.model_store.get(r.IMAGE_SOURCE_MODEL).target_model == r.FLUX_TARGET_MODEL
|
||||
|
||||
|
||||
def test_seed_default_deepseek_routes_is_idempotent_and_preserves_customization(
|
||||
local_db,
|
||||
):
|
||||
r.seed_default_deepseek_routes()
|
||||
flash = r.model_store.get("deepseek-v4-flash")
|
||||
pro = r.model_store.get("deepseek-v4-pro")
|
||||
assert flash is not None and pro is not None
|
||||
assert flash.price_cache_hit_per_m == 0.0028
|
||||
assert flash.price_cache_miss_per_m == 0.14
|
||||
assert flash.price_output_per_m == 0.28
|
||||
assert flash.context_window == 1_048_576
|
||||
assert pro.price_cache_hit_per_m == 0.003625
|
||||
assert pro.price_cache_miss_per_m == 0.435
|
||||
assert pro.price_output_per_m == 0.87
|
||||
r.model_store.set(
|
||||
r.ModelRouteIn(
|
||||
source_model="deepseek-v4-pro",
|
||||
target_model="deepseek-v4-pro",
|
||||
price_output_per_m=1.23,
|
||||
)
|
||||
)
|
||||
try:
|
||||
r.seed_default_deepseek_routes()
|
||||
assert r.model_store.get("deepseek-v4-pro").price_output_per_m == 1.23
|
||||
finally:
|
||||
r.model_store.set(
|
||||
r.ModelRouteIn(
|
||||
source_model="deepseek-v4-pro",
|
||||
target_model="deepseek-v4-pro",
|
||||
price_cache_hit_per_m=0.003625,
|
||||
price_cache_miss_per_m=0.435,
|
||||
price_output_per_m=0.87,
|
||||
context_window=1_048_576,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
from dataclasses import replace
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from devplacepy.services.openai_gateway.usage import (
|
||||
USAGE_FIELDS,
|
||||
Pricing,
|
||||
accumulate_usage,
|
||||
compute_cost,
|
||||
new_usage_totals,
|
||||
normalize_usage,
|
||||
parse_usage_headers,
|
||||
usage_metric_cards,
|
||||
)
|
||||
|
||||
BASE_PRICING = Pricing(
|
||||
chat_cache_hit_per_m=0.0028,
|
||||
chat_cache_miss_per_m=0.14,
|
||||
chat_output_per_m=0.28,
|
||||
vision_input_per_m=0.0,
|
||||
vision_output_per_m=0.0,
|
||||
embed_input_per_m=0.01,
|
||||
)
|
||||
|
||||
GATEWAY_HEADERS = {
|
||||
"X-Gateway-Cost-USD": "0.00010000",
|
||||
"X-Gateway-Model": "molodetz",
|
||||
@@ -89,3 +104,118 @@ def test_usage_metric_cards_labels_and_formatting():
|
||||
assert by_label["Total cost"] == "$0.0492"
|
||||
assert by_label["Avg cost/call"] == "$0.012300"
|
||||
assert by_label["Avg latency"] == "4200ms"
|
||||
|
||||
|
||||
def test_compute_cost_matches_deepseek_flat_rates_when_no_tier_configured():
|
||||
norm = normalize_usage(
|
||||
{
|
||||
"prompt_tokens": 2000,
|
||||
"completion_tokens": 100,
|
||||
"prompt_cache_hit_tokens": 500,
|
||||
"prompt_cache_miss_tokens": 1500,
|
||||
}
|
||||
)
|
||||
total, input_cost, output_cost, native = compute_cost({}, norm, BASE_PRICING, "chat")
|
||||
assert native is False
|
||||
assert abs(input_cost - (500 / 1e6 * 0.0028 + 1500 / 1e6 * 0.14)) < 1e-12
|
||||
assert abs(output_cost - (100 / 1e6 * 0.28)) < 1e-12
|
||||
assert abs(total - (input_cost + output_cost)) < 1e-12
|
||||
|
||||
|
||||
def test_compute_cost_switches_to_tier2_above_threshold():
|
||||
tiered = replace(
|
||||
BASE_PRICING,
|
||||
chat_cache_hit_per_m_tier2=0.005,
|
||||
chat_cache_miss_per_m_tier2=0.25,
|
||||
chat_output_per_m_tier2=0.5,
|
||||
context_tier_threshold_tokens=1000,
|
||||
)
|
||||
below = normalize_usage({"prompt_tokens": 900, "completion_tokens": 50})
|
||||
above = normalize_usage({"prompt_tokens": 1001, "completion_tokens": 50})
|
||||
_, below_input, below_output, _ = compute_cost({}, below, tiered, "chat")
|
||||
_, above_input, above_output, _ = compute_cost({}, above, tiered, "chat")
|
||||
assert abs(below_output - (50 / 1e6 * 0.28)) < 1e-12
|
||||
assert abs(above_output - (50 / 1e6 * 0.5)) < 1e-12
|
||||
assert below_input != above_input
|
||||
|
||||
|
||||
def test_compute_cost_tier2_leaves_unset_component_at_tier1():
|
||||
tiered = replace(
|
||||
BASE_PRICING,
|
||||
chat_output_per_m_tier2=0.5,
|
||||
context_tier_threshold_tokens=100,
|
||||
)
|
||||
norm = normalize_usage({"prompt_tokens": 200, "completion_tokens": 10})
|
||||
_, input_cost, output_cost, _ = compute_cost({}, norm, tiered, "chat")
|
||||
assert abs(output_cost - (10 / 1e6 * 0.5)) < 1e-12
|
||||
assert abs(input_cost - (200 / 1e6 * 0.14)) < 1e-12
|
||||
|
||||
|
||||
def test_compute_cost_applies_off_peak_discount():
|
||||
discounted = replace(
|
||||
BASE_PRICING,
|
||||
off_peak_start_minute=60,
|
||||
off_peak_end_minute=120,
|
||||
off_peak_discount_pct=50.0,
|
||||
)
|
||||
norm = normalize_usage({"prompt_tokens": 1000, "completion_tokens": 100})
|
||||
in_window = datetime(2026, 1, 1, 1, 30, tzinfo=timezone.utc)
|
||||
outside_window = datetime(2026, 1, 1, 10, 0, tzinfo=timezone.utc)
|
||||
total_in, _, _, _ = compute_cost({}, norm, discounted, "chat", now=in_window)
|
||||
total_out, _, _, _ = compute_cost({}, norm, discounted, "chat", now=outside_window)
|
||||
total_flat, _, _, _ = compute_cost({}, norm, BASE_PRICING, "chat")
|
||||
assert abs(total_out - total_flat) < 1e-12
|
||||
assert abs(total_in - total_flat / 2) < 1e-12
|
||||
|
||||
|
||||
def test_compute_cost_off_peak_wraps_past_midnight():
|
||||
wrapped = replace(
|
||||
BASE_PRICING,
|
||||
off_peak_start_minute=23 * 60,
|
||||
off_peak_end_minute=60,
|
||||
off_peak_discount_pct=100.0,
|
||||
)
|
||||
norm = normalize_usage({"prompt_tokens": 1000, "completion_tokens": 100})
|
||||
just_after_midnight = datetime(2026, 1, 1, 0, 30, tzinfo=timezone.utc)
|
||||
midday = datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)
|
||||
total_wrapped, _, _, _ = compute_cost(
|
||||
{}, norm, wrapped, "chat", now=just_after_midnight
|
||||
)
|
||||
total_midday, _, _, _ = compute_cost({}, norm, wrapped, "chat", now=midday)
|
||||
assert total_wrapped == 0.0
|
||||
assert total_midday > 0.0
|
||||
|
||||
|
||||
def test_compute_cost_image_branch_flat_per_call():
|
||||
image_pricing = replace(BASE_PRICING, image_per_call=0.04)
|
||||
norm = normalize_usage({})
|
||||
total, input_cost, output_cost, native = compute_cost(
|
||||
{}, norm, image_pricing, "image"
|
||||
)
|
||||
assert native is False
|
||||
assert output_cost == 0.0
|
||||
assert abs(total - 0.04) < 1e-9
|
||||
assert abs(input_cost - 0.04) < 1e-9
|
||||
|
||||
|
||||
def test_compute_cost_image_native_cost_preferred():
|
||||
from devplacepy.services.openai_gateway.usage import extract_image_usage
|
||||
|
||||
image_pricing = replace(BASE_PRICING, image_per_call=0.04)
|
||||
norm = normalize_usage({})
|
||||
usage = extract_image_usage({"usage": {"cost": 0.055}})
|
||||
total, _, _, native = compute_cost(usage, norm, image_pricing, "image")
|
||||
assert native is True
|
||||
assert total == 0.055
|
||||
|
||||
|
||||
def test_compute_cost_native_upstream_cost_unaffected_by_tiering():
|
||||
tiered = replace(
|
||||
BASE_PRICING,
|
||||
chat_output_per_m_tier2=100.0,
|
||||
context_tier_threshold_tokens=1,
|
||||
)
|
||||
norm = normalize_usage({"prompt_tokens": 1000, "completion_tokens": 100})
|
||||
total, _, _, native = compute_cost({"cost": 0.05}, norm, tiered, "chat")
|
||||
assert native is True
|
||||
assert total == 0.05
|
||||
|
||||
Reference in New Issue
Block a user