diff --git a/bc2/core/common/azure_pricing.py b/bc2/core/common/azure_pricing.py index b80963b..4485429 100644 --- a/bc2/core/common/azure_pricing.py +++ b/bc2/core/common/azure_pricing.py @@ -37,7 +37,7 @@ def estimate( self, call: dict[str, Any], runtime_config: dict[str, Any] ) -> dict[str, Any]: """Estimate the cost of one Azure service call.""" - region = runtime_config.get("azure_region") + region = runtime_config.get("azure_region") or call.get("azure_region") if not region: raise AzurePricingUnavailable( "azure_region is required to look up Azure retail pricing" diff --git a/bc2/core/common/openai.py b/bc2/core/common/openai.py index ee9d280..de329e9 100644 --- a/bc2/core/common/openai.py +++ b/bc2/core/common/openai.py @@ -598,6 +598,7 @@ def _record_response_usage( "model": config.openai_model or (reported_model if isinstance(reported_model, str) else config.model), "deployment": config.model if provider == "azure" else None, + "azure_region": _azure_region(client) if provider == "azure" else None, "response_id": getattr(response, "id", None), "status": getattr(response, "status", None), "usage": token_usage, @@ -618,5 +619,17 @@ def _openai_provider(client: OpenAI | AsyncOpenAI) -> str: return "openai" +def _azure_region(client: OpenAI | AsyncOpenAI) -> str | None: + """Infer the region suffix from an Azure OpenAI resource hostname.""" + base_url = str(getattr(client, "base_url", "")) + host = (urlparse(base_url).hostname or "").lower() + suffix = ".openai.azure.com" + if not host.endswith(suffix): + return None + + resource_name = host.removesuffix(suffix).rsplit(".", 1)[-1] + return resource_name.rsplit("-", 1)[-1] or None + + class OpenAIConfig(BaseModel): client: OpenAIClientConfig diff --git a/bc2/core/common/test_azure_pricing.py b/bc2/core/common/test_azure_pricing.py index 44f073e..3e45b14 100644 --- a/bc2/core/common/test_azure_pricing.py +++ b/bc2/core/common/test_azure_pricing.py @@ -115,6 +115,30 @@ def test_missing_region_fails_without_fetching_prices(): ) +def test_region_falls_back_to_usage_call(monkeypatch): + pricing = AzureRetailPricing() + monkeypatch.setattr( + pricing, + "_estimate_openai_tokens", + lambda call, runtime_config, region: { + "estimated_cost": 0.0, + "currency": "USD", + }, + ) + + estimate = pricing.estimate( + { + "service": "responses", + "model": "gpt-4.1", + "azure_region": "eastus", + "usage": {"input_tokens": 10}, + }, + {}, + ) + + assert estimate["region"] == "eastus" + + def test_ambiguous_meter_fails_gracefully(monkeypatch): pricing = AzureRetailPricing() meters = [ diff --git a/bc2/core/common/test_openai.py b/bc2/core/common/test_openai.py index ffb71b3..14bca4c 100644 --- a/bc2/core/common/test_openai.py +++ b/bc2/core/common/test_openai.py @@ -316,7 +316,7 @@ def test_invoke_completed_response_is_not_truncated(): def test_invoke_records_response_usage(): cfg = _build_chat_config() client = MagicMock() - client.base_url = "https://example.openai.azure.com/openai/v1/" + client.base_url = "https://hks-cpl-blindcharging-eastus.openai.azure.com/openai/v1/" client.responses.create.return_value = _mock_response( status="completed", output_text="full answer", @@ -331,6 +331,7 @@ def test_invoke_records_response_usage(): call = report["calls"][0] assert call["provider"] == "azure" + assert call["azure_region"] == "eastus" assert call["service"] == "responses" assert call["operation"] == "parse:openai" assert call["response_id"] == "resp_test"