"""Step definitions for cost controls feature tests.""" from __future__ import annotations import os from typing import Any from behave import given, then, when # type: ignore[import-untyped] from behave.runner import Context # type: ignore[import-untyped] # ---- CostEntry steps ---- @given("I import the CostEntry class") def step_import_cost_entry(context: Context) -> None: from cleveragents.providers.cost_table import CostEntry context.CostEntry = CostEntry @when("I create a CostEntry with negative input cost") def step_create_cost_entry_negative_input(context: Context) -> None: context.cost_entry_error = None try: context.CostEntry(input_cost_per_token=-0.001, output_cost_per_token=0.001) except ValueError as exc: context.cost_entry_error = exc @when("I create a CostEntry with negative output cost") def step_create_cost_entry_negative_output(context: Context) -> None: context.cost_entry_error = None try: context.CostEntry(input_cost_per_token=0.001, output_cost_per_token=-0.001) except ValueError as exc: context.cost_entry_error = exc @then("a ValueError should be raised for CostEntry") def step_cost_entry_valueerror(context: Context) -> None: assert context.cost_entry_error is not None, "Expected ValueError" assert isinstance(context.cost_entry_error, ValueError) @when( "I create a CostEntry with input cost {input_cost:f} and output cost {output_cost:f}" ) def step_create_cost_entry_with_costs( context: Context, input_cost: float, output_cost: float ) -> None: context.cost_entry = context.CostEntry( input_cost_per_token=input_cost, output_cost_per_token=output_cost, ) @when( "I estimate cost for {input_tokens:d} input tokens and {output_tokens:d} output tokens" ) def step_estimate_cost(context: Context, input_tokens: int, output_tokens: int) -> None: context.estimated_cost = context.cost_entry.estimate_cost( input_tokens, output_tokens ) @then("the estimated cost should be {expected:f}") def step_check_estimated_cost(context: Context, expected: float) -> None: assert abs(context.estimated_cost - expected) < 1e-9, ( f"Expected {expected}, got {context.estimated_cost}" ) @then("estimating cost with negative input tokens raises ValueError") def step_estimate_negative_input(context: Context) -> None: try: context.cost_entry.estimate_cost(-1, 0) raise AssertionError("Expected ValueError") except ValueError: pass @then("estimating cost with negative output tokens raises ValueError") def step_estimate_negative_output(context: Context) -> None: try: context.cost_entry.estimate_cost(0, -1) raise AssertionError("Expected ValueError") except ValueError: pass # ---- ProviderCostTable steps ---- @given("I create a ProviderCostTable with defaults") def step_create_default_cost_table(context: Context) -> None: from cleveragents.providers.cost_table import ProviderCostTable context.cost_table = ProviderCostTable() @when('I look up cost for provider "{provider}" model "{model}"') def step_lookup_cost(context: Context, provider: str, model: str) -> None: context.cost_entry_result = context.cost_table.get_cost_entry(provider, model) @then("the cost entry should be the default cost") def step_check_default_cost(context: Context) -> None: from cleveragents.providers.cost_table import ProviderCostTable assert context.cost_entry_result == ProviderCostTable.DEFAULT_COST @then("the cost entry input cost should be {expected:f}") def step_check_input_cost(context: Context, expected: float) -> None: assert abs(context.cost_entry_result.input_cost_per_token - expected) < 1e-12, ( f"Expected {expected}, got {context.cost_entry_result.input_cost_per_token}" ) @then("the cost entry output cost should be {expected:f}") def step_check_output_cost(context: Context, expected: float) -> None: assert abs(context.cost_entry_result.output_cost_per_token - expected) < 1e-12, ( f"Expected {expected}, got {context.cost_entry_result.output_cost_per_token}" ) @given('I create a ProviderCostTable with custom entry for "{provider}" "{model}"') def step_create_custom_cost_table(context: Context, provider: str, model: str) -> None: from cleveragents.providers.cost_table import CostEntry, ProviderCostTable custom: dict[str, dict[str, CostEntry]] = { provider: { model: CostEntry(input_cost_per_token=0.001, output_cost_per_token=0.002) } } context.cost_table = ProviderCostTable(custom_entries=custom) @then("looking up cost for empty provider raises ValueError") def step_lookup_empty_provider(context: Context) -> None: try: context.cost_table.get_cost_entry("", "model") raise AssertionError("Expected ValueError") except ValueError: pass @then("looking up cost for empty model raises ValueError") def step_lookup_empty_model(context: Context) -> None: try: context.cost_table.get_cost_entry("openai", "") raise AssertionError("Expected ValueError") except ValueError: pass @when("I list providers from the cost table") def step_list_providers(context: Context) -> None: context.provider_list = context.cost_table.list_providers() @then('the provider list should include "{provider}"') def step_check_provider_in_list(context: Context, provider: str) -> None: assert provider in context.provider_list, ( f"{provider} not in {context.provider_list}" ) @when('I list models for provider "{provider}"') def step_list_models(context: Context, provider: str) -> None: context.model_list = context.cost_table.list_models(provider) @then('the model list should include "{model}"') def step_check_model_in_list(context: Context, model: str) -> None: assert model in context.model_list, f"{model} not in {context.model_list}" @when("I create a ProviderCostTable with non-dict custom entries") def step_create_cost_table_non_dict(context: Context) -> None: from cleveragents.providers.cost_table import ProviderCostTable context.cost_table_error = None try: ProviderCostTable(custom_entries="not-a-dict") # type: ignore[arg-type] except TypeError as exc: context.cost_table_error = exc @then("a TypeError should be raised for ProviderCostTable") def step_cost_table_typeerror(context: Context) -> None: assert context.cost_table_error is not None, "Expected TypeError" assert isinstance(context.cost_table_error, TypeError) # ---- CostMetadata steps ---- @given("I create a fresh CostMetadata") def step_create_cost_metadata(context: Context) -> None: from cleveragents.domain.models.core.cost_metadata import CostMetadata context.cost_metadata = CostMetadata() @when( "I record usage of {input_t:d} input tokens and {output_t:d} output tokens " 'at cost {cost:f} for provider "{provider}"' ) def step_record_usage( context: Context, input_t: int, output_t: int, cost: float, provider: str ) -> None: context.cost_metadata.record_usage( input_tokens=input_t, output_tokens=output_t, cost=cost, provider=provider, ) @then("total tokens should be {expected:d}") def step_check_total_tokens(context: Context, expected: int) -> None: assert context.cost_metadata.total_tokens == expected, ( f"Expected {expected}, got {context.cost_metadata.total_tokens}" ) @then("total cost should be {expected:f}") def step_check_total_cost(context: Context, expected: float) -> None: assert abs(context.cost_metadata.total_cost - expected) < 1e-9, ( f"Expected {expected}, got {context.cost_metadata.total_cost}" ) @then('provider costs for "{provider}" should be {expected:f}') def step_check_provider_costs(context: Context, provider: str, expected: float) -> None: actual = context.cost_metadata.provider_costs.get(provider, 0.0) assert abs(actual - expected) < 1e-9, f"Expected {expected}, got {actual}" @then("recording usage with negative input tokens raises ValueError") def step_record_negative_input(context: Context) -> None: try: context.cost_metadata.record_usage( input_tokens=-1, output_tokens=0, cost=0.0, provider="test" ) raise AssertionError("Expected ValueError") except ValueError: pass @then("recording usage with negative output tokens raises ValueError") def step_record_negative_output_tokens(context: Context) -> None: try: context.cost_metadata.record_usage( input_tokens=0, output_tokens=-1, cost=0.0, provider="test" ) raise AssertionError("Expected ValueError") except ValueError: pass @then("recording usage with negative cost raises ValueError") def step_record_negative_cost(context: Context) -> None: try: context.cost_metadata.record_usage( input_tokens=0, output_tokens=0, cost=-1.0, provider="test" ) raise AssertionError("Expected ValueError") except ValueError: pass @then("recording usage with empty provider raises ValueError") def step_record_empty_provider(context: Context) -> None: try: context.cost_metadata.record_usage( input_tokens=0, output_tokens=0, cost=0.0, provider="" ) raise AssertionError("Expected ValueError") except ValueError: pass @when("I get the display dict from cost metadata") def step_get_display_dict(context: Context) -> None: context.display_dict = context.cost_metadata.as_display_dict() @then('the display dict should have key "{key}"') def step_check_display_dict_key(context: Context, key: str) -> None: assert key in context.display_dict, f"Key {key} not in {context.display_dict}" @when("I set budget remaining to {value:f}") def step_set_budget_remaining(context: Context, value: float) -> None: context.cost_metadata.budget_remaining = value # ---- BudgetExhaustionEvent steps ---- @given("I import the BudgetExhaustionEvent class") def step_import_exhaustion_event(context: Context) -> None: from cleveragents.domain.models.core.cost_metadata import BudgetExhaustionEvent context.BudgetExhaustionEvent = BudgetExhaustionEvent @when("I create a BudgetExhaustionEvent with invalid budget type") def step_create_invalid_event(context: Context) -> None: context.exhaustion_event_error = None try: context.BudgetExhaustionEvent(budget_type="invalid", limit=10.0, used=5.0) except Exception as exc: context.exhaustion_event_error = exc @then("a validation error should be raised for BudgetExhaustionEvent") def step_check_exhaustion_validation_error(context: Context) -> None: assert context.exhaustion_event_error is not None, "Expected validation error" @when('I create a BudgetExhaustionEvent with budget type "{btype}"') def step_create_valid_event(context: Context, btype: str) -> None: context.exhaustion_event = context.BudgetExhaustionEvent( budget_type=btype, limit=10.0, used=5.0 ) @then("the event should be created successfully") def step_check_event_created(context: Context) -> None: assert context.exhaustion_event is not None # ---- CostTracker steps ---- @given("I create a CostTracker with no limits") def step_create_tracker_no_limits(context: Context) -> None: from cleveragents.providers.cost_tracker import CostTracker context.cost_tracker = CostTracker() @given("I create a CostTracker with plan budget {budget:f}") def step_create_tracker_plan_budget(context: Context, budget: float) -> None: from cleveragents.providers.cost_tracker import CostTracker context.cost_tracker = CostTracker(budget_per_plan=budget) @given("I create a CostTracker with daily budget {budget:f}") def step_create_tracker_daily_budget(context: Context, budget: float) -> None: from cleveragents.providers.cost_tracker import CostTracker context.cost_tracker = CostTracker(budget_per_day=budget) context.fallback_cost_tracker = context.cost_tracker @given("I create a CostTracker with plan budget {plan:f} and daily budget {daily:f}") def step_create_tracker_both_budgets( context: Context, plan: float, daily: float ) -> None: from cleveragents.providers.cost_tracker import CostTracker context.cost_tracker = CostTracker(budget_per_plan=plan, budget_per_day=daily) @when( "I record tracked usage of {inp:d} input and {out:d} output " 'for "{provider}" "{model}"' ) def step_record_tracked_usage( context: Context, inp: int, out: int, provider: str, model: str ) -> None: if not hasattr(context, "cost_metadata"): from cleveragents.domain.models.core.cost_metadata import CostMetadata context.cost_metadata = CostMetadata() context.budget_result = context.cost_tracker.record_usage( context.cost_metadata, provider=provider, model=model, input_tokens=inp, output_tokens=out, ) @then("the budget check should be under budget") def step_check_under_budget(context: Context) -> None: from cleveragents.providers.cost_tracker import BudgetStatus assert context.budget_result.status == BudgetStatus.UNDER_BUDGET, ( f"Expected UNDER_BUDGET, got {context.budget_result.status}" ) @then("the budget check should be warning or exceeded") def step_check_warning_or_exceeded(context: Context) -> None: from cleveragents.providers.cost_tracker import BudgetStatus assert context.budget_result.status in ( BudgetStatus.WARNING, BudgetStatus.EXCEEDED, ), f"Expected WARNING or EXCEEDED, got {context.budget_result.status}" @then("the budget check should be exceeded") def step_check_exceeded(context: Context) -> None: from cleveragents.providers.cost_tracker import BudgetStatus assert context.budget_result.status == BudgetStatus.EXCEEDED, ( f"Expected EXCEEDED, got {context.budget_result.status}" ) @then("the cost metadata total tokens should be {expected:d}") def step_check_metadata_total_tokens(context: Context, expected: int) -> None: assert context.cost_metadata.total_tokens == expected, ( f"Expected {expected}, got {context.cost_metadata.total_tokens}" ) @then("the cost metadata should have budget exhaustion events") def step_check_exhaustion_events(context: Context) -> None: assert len(context.cost_metadata.budget_exhaustion_events) > 0, ( "Expected budget exhaustion events" ) @when("I create a CostTracker with negative plan budget") def step_create_tracker_negative_plan(context: Context) -> None: from cleveragents.providers.cost_tracker import CostTracker context.tracker_error = None try: CostTracker(budget_per_plan=-1.0) except ValueError as exc: context.tracker_error = exc @when("I create a CostTracker with negative daily budget") def step_create_tracker_negative_daily(context: Context) -> None: from cleveragents.providers.cost_tracker import CostTracker context.tracker_error = None try: CostTracker(budget_per_day=-1.0) except ValueError as exc: context.tracker_error = exc @then("a ValueError should be raised for CostTracker") def step_check_tracker_valueerror(context: Context) -> None: assert context.tracker_error is not None, "Expected ValueError" assert isinstance(context.tracker_error, ValueError) @when( 'I estimate cost for "{provider}" "{model}" with {inp:d} input and {out:d} output' ) def step_estimate_tracker_cost( context: Context, provider: str, model: str, inp: int, out: int ) -> None: context.tracker_estimated_cost = context.cost_tracker.estimate_cost( provider, model, inp, out ) @then("the estimated tracker cost should be positive") def step_check_tracker_positive_cost(context: Context) -> None: assert context.tracker_estimated_cost > 0.0, ( f"Expected positive cost, got {context.tracker_estimated_cost}" ) @then("estimating cost with empty provider raises ValueError on tracker") def step_estimate_empty_provider_tracker(context: Context) -> None: try: context.cost_tracker.estimate_cost("", "gpt-4o", 100, 50) raise AssertionError("Expected ValueError") except ValueError: pass @then("estimating cost with empty model raises ValueError on tracker") def step_estimate_empty_model_tracker(context: Context) -> None: try: context.cost_tracker.estimate_cost("openai", "", 100, 50) raise AssertionError("Expected ValueError") except ValueError: pass @then("recording tracked usage with negative input raises ValueError") def step_record_tracked_negative_input(context: Context) -> None: try: context.cost_tracker.record_usage( context.cost_metadata, provider="openai", model="gpt-4o", input_tokens=-1, output_tokens=0, ) raise AssertionError("Expected ValueError") except ValueError: pass @when("I check plan budget") def step_check_plan_budget(context: Context) -> None: context.budget_result = context.cost_tracker.check_plan_budget( context.cost_metadata ) @when("I check daily budget") def step_check_daily_budget(context: Context) -> None: context.budget_result = context.cost_tracker.check_daily_budget() @then("the plan budget check should be under budget") def step_plan_under_budget(context: Context) -> None: from cleveragents.providers.cost_tracker import BudgetStatus assert context.budget_result.status == BudgetStatus.UNDER_BUDGET @then("the plan budget check should be exceeded") def step_plan_exceeded(context: Context) -> None: from cleveragents.providers.cost_tracker import BudgetStatus assert context.budget_result.status == BudgetStatus.EXCEEDED @then("the daily budget check should be under budget") def step_daily_under_budget(context: Context) -> None: from cleveragents.providers.cost_tracker import BudgetStatus assert context.budget_result.status == BudgetStatus.UNDER_BUDGET @then("the daily budget check should be exceeded") def step_daily_exceeded(context: Context) -> None: from cleveragents.providers.cost_tracker import BudgetStatus assert context.budget_result.status == BudgetStatus.EXCEEDED @then("the daily spend should be {expected:f}") def step_check_daily_spend(context: Context, expected: float) -> None: actual = context.cost_tracker.get_daily_spend() assert abs(actual - expected) < 1e-9, f"Expected {expected}, got {actual}" @then("the tracker plan budget should be {expected:f}") def step_check_tracker_plan_budget(context: Context, expected: float) -> None: assert context.cost_tracker.budget_per_plan == expected @then("the tracker daily budget should be {expected:f}") def step_check_tracker_daily_budget(context: Context, expected: float) -> None: assert context.cost_tracker.budget_per_day == expected @then("getting cost entry with empty provider raises ValueError") def step_get_cost_entry_empty_provider(context: Context) -> None: try: context.cost_tracker.get_cost_entry("", "gpt-4o") raise AssertionError("Expected ValueError") except ValueError: pass @then("getting cost entry with empty model raises ValueError") def step_get_cost_entry_empty_model(context: Context) -> None: try: context.cost_tracker.get_cost_entry("openai", "") raise AssertionError("Expected ValueError") except ValueError: pass @then("recording tracked usage with empty provider raises ValueError") def step_record_tracked_empty_provider(context: Context) -> None: try: context.cost_tracker.record_usage( context.cost_metadata, provider="", model="gpt-4o", input_tokens=1, output_tokens=1, ) raise AssertionError("Expected ValueError") except ValueError: pass @then("recording tracked usage with empty model raises ValueError") def step_record_tracked_empty_model(context: Context) -> None: try: context.cost_tracker.record_usage( context.cost_metadata, provider="openai", model="", input_tokens=1, output_tokens=1, ) raise AssertionError("Expected ValueError") except ValueError: pass # ---- FallbackSelector steps ---- @given("I create a ProviderRegistry with no API keys for fallback") def step_create_registry_no_keys(context: Context) -> None: import os from cleveragents.config.settings import Settings from cleveragents.providers.registry import ProviderRegistry # Temporarily clear all provider API keys to simulate no configured providers key_env_vars = [ "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GOOGLE_API_KEY", "AZURE_OPENAI_API_KEY", "OPENROUTER_API_KEY", "GEMINI_API_KEY", "GOOGLE_GENAI_API_KEY", "GROQ_API_KEY", "TOGETHER_API_KEY", "COHERE_API_KEY", ] saved: dict[str, str | None] = {} for var in key_env_vars: saved[var] = os.environ.pop(var, None) try: settings = Settings() context.fallback_registry = ProviderRegistry(settings) finally: for var, val in saved.items(): if val is not None: os.environ[var] = val @given("I create a ProviderRegistry with mock provider for fallback") def step_create_registry_mock(context: Context) -> None: import os from cleveragents.config.settings import Settings from cleveragents.providers.registry import ProviderRegistry # Clear all real API keys, the mock provider is always discovered key_env_vars = [ "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GOOGLE_API_KEY", "AZURE_OPENAI_API_KEY", "OPENROUTER_API_KEY", "GEMINI_API_KEY", "GOOGLE_GENAI_API_KEY", "GROQ_API_KEY", "TOGETHER_API_KEY", "COHERE_API_KEY", ] saved: dict[str, str | None] = {} for var in key_env_vars: saved[var] = os.environ.pop(var, None) try: settings = Settings() context.fallback_registry = ProviderRegistry(settings) finally: for var, val in saved.items(): if val is not None: os.environ[var] = val @given("I create a ProviderRegistry with openai key for fallback") def step_create_registry_openai(context: Context) -> None: from cleveragents.config.settings import Settings from cleveragents.providers.registry import ProviderRegistry old_key = os.environ.get("OPENAI_API_KEY") os.environ["OPENAI_API_KEY"] = "test-key-for-fallback" try: settings = Settings() context.fallback_registry = ProviderRegistry(settings) finally: if old_key is None: os.environ.pop("OPENAI_API_KEY", None) else: os.environ["OPENAI_API_KEY"] = old_key @when("I create a FallbackSelector with the registry") def step_create_fallback_selector(context: Context) -> None: from cleveragents.providers.fallback_selector import FallbackSelector tracker = getattr(context, "fallback_cost_tracker", None) context.fallback_selector = FallbackSelector( registry=context.fallback_registry, cost_tracker=tracker, ) @when("I select a fallback provider") def step_select_fallback(context: Context) -> None: context.fallback_result = context.fallback_selector.select() @when("I select a fallback provider requiring tool calls") def step_select_fallback_tool_calls(context: Context) -> None: context.fallback_result = context.fallback_selector.select(require_tool_calls=True) @then("the fallback result provider should be None") def step_check_fallback_none(context: Context) -> None: assert context.fallback_result.provider_type is None, ( f"Expected None, got {context.fallback_result.provider_type}" ) @then("the fallback result should have skipped entries") def step_check_fallback_skipped(context: Context) -> None: assert len(context.fallback_result.skipped) > 0 @then("the fallback result should skip mock for missing tool calls") def step_check_mock_skipped_tool_calls(context: Context) -> None: # All configured providers were skipped (none configured or missing caps) assert len(context.fallback_result.skipped) > 0 @then('the fallback result provider type should be "{expected}"') def step_check_fallback_provider_type(context: Context, expected: str) -> None: assert context.fallback_result.provider_type is not None assert context.fallback_result.provider_type.value == expected, ( f"Expected {expected}, got {context.fallback_result.provider_type.value}" ) @when('I create a FallbackSelector with custom order "{providers}"') def step_create_fallback_custom_order(context: Context, providers: str) -> None: from cleveragents.providers.fallback_selector import FallbackSelector provider_list = [p.strip() for p in providers.split(",")] context.fallback_selector = FallbackSelector( registry=context.fallback_registry, fallback_providers=provider_list, ) @when("I create a FallbackSelector with None registry") def step_create_fallback_none_registry(context: Context) -> None: from cleveragents.providers.fallback_selector import FallbackSelector context.fallback_error = None try: FallbackSelector(registry=None) # type: ignore[arg-type] except TypeError as exc: context.fallback_error = exc @then("a TypeError should be raised for FallbackSelector") def step_check_fallback_typeerror(context: Context) -> None: assert context.fallback_error is not None, "Expected TypeError" assert isinstance(context.fallback_error, TypeError) @when("I create a FallbackSelector with empty string in fallback list") def step_create_fallback_empty_string(context: Context) -> None: from cleveragents.providers.fallback_selector import FallbackSelector context.fallback_error = None try: FallbackSelector( registry=context.fallback_registry, fallback_providers=["openai", ""], ) except ValueError as exc: context.fallback_error = exc @then("a ValueError should be raised for FallbackSelector") def step_check_fallback_valueerror(context: Context) -> None: assert context.fallback_error is not None, "Expected ValueError" assert isinstance(context.fallback_error, ValueError) @when("I create a FallbackSelector with cost tracker") def step_create_fallback_with_tracker(context: Context) -> None: from cleveragents.providers.fallback_selector import FallbackSelector context.fallback_selector = FallbackSelector( registry=context.fallback_registry, cost_tracker=context.fallback_cost_tracker, ) # ---- Settings config keys ---- @when("I load cost control settings with defaults") def step_load_default_settings(context: Context) -> None: from cleveragents.config.settings import Settings context.cost_settings = Settings() @then("budget_per_plan should be None") def step_check_budget_per_plan_none(context: Context) -> None: assert context.cost_settings.budget_per_plan is None @then("budget_per_day should be None") def step_check_budget_per_day_none(context: Context) -> None: assert context.cost_settings.budget_per_day is None @then("fallback_providers should be an empty list") def step_check_fallback_empty(context: Context) -> None: assert context.cost_settings.fallback_providers == [] @when("I load cost control settings with budget_per_plan {value:f}") def step_load_settings_plan_budget(context: Context, value: float) -> None: from cleveragents.config.settings import Settings old = os.environ.get("CLEVERAGENTS_BUDGET_PER_PLAN") os.environ["CLEVERAGENTS_BUDGET_PER_PLAN"] = str(value) try: context.cost_settings = Settings() finally: if old is None: os.environ.pop("CLEVERAGENTS_BUDGET_PER_PLAN", None) else: os.environ["CLEVERAGENTS_BUDGET_PER_PLAN"] = old @then("budget_per_plan should be {expected:f}") def step_check_budget_per_plan_value(context: Context, expected: float) -> None: assert context.cost_settings.budget_per_plan == expected, ( f"Expected {expected}, got {context.cost_settings.budget_per_plan}" ) @when("I load cost control settings with budget_per_day {value:f}") def step_load_settings_daily_budget(context: Context, value: float) -> None: from cleveragents.config.settings import Settings old = os.environ.get("CLEVERAGENTS_BUDGET_PER_DAY") os.environ["CLEVERAGENTS_BUDGET_PER_DAY"] = str(value) try: context.cost_settings = Settings() finally: if old is None: os.environ.pop("CLEVERAGENTS_BUDGET_PER_DAY", None) else: os.environ["CLEVERAGENTS_BUDGET_PER_DAY"] = old @then("budget_per_day should be {expected:f}") def step_check_budget_per_day_value(context: Context, expected: float) -> None: assert context.cost_settings.budget_per_day == expected, ( f"Expected {expected}, got {context.cost_settings.budget_per_day}" ) # ---- Plan model steps ---- def _make_minimal_plan( cost_meta: Any = None, ) -> Any: """Create a minimal Plan v3 model for testing.""" from cleveragents.domain.models.core.plan import ( NamespacedName, Plan, PlanIdentity, ) return Plan( identity=PlanIdentity(plan_id="01ARZJG0J0000000000000000A"), namespaced_name=NamespacedName(namespace="local", name="test-plan"), description="Test plan for cost metadata", action_name="local/test-action", cost_metadata=cost_meta, ) @given("I create a minimal Plan with cost metadata") def step_create_plan_with_cost(context: Context) -> None: from cleveragents.domain.models.core.cost_metadata import CostMetadata meta = CostMetadata(total_tokens=100, total_cost=0.01) context.plan_with_cost = _make_minimal_plan(cost_meta=meta) @then("the plan should have cost_metadata set") def step_check_plan_cost_metadata(context: Context) -> None: assert context.plan_with_cost.cost_metadata is not None @when("I get the CLI dict from the plan") def step_get_cli_dict(context: Context) -> None: plan = getattr(context, "plan_with_cost", None) or getattr( context, "plan_without_cost", None ) context.cli_dict = plan.as_cli_dict() @then('the CLI dict should have a "{key}" key') def step_check_cli_dict_has_key(context: Context, key: str) -> None: assert key in context.cli_dict, f"Key {key} not in {list(context.cli_dict.keys())}" @given("I create a minimal Plan without cost metadata") def step_create_plan_without_cost(context: Context) -> None: context.plan_without_cost = _make_minimal_plan(cost_meta=None) @then('the CLI dict should not have a "{key}" key') def step_check_cli_dict_no_key(context: Context, key: str) -> None: assert key not in context.cli_dict, f"Key {key} unexpectedly in CLI dict" @when('I add a budget exhaustion event of type "{btype}"') def step_add_exhaustion_event(context: Context, btype: str) -> None: from cleveragents.domain.models.core.cost_metadata import BudgetExhaustionEvent event = BudgetExhaustionEvent( budget_type=btype, limit=10.0, used=11.0, provider="openai", model="gpt-4o", ) context.cost_metadata.budget_exhaustion_events.append(event) # ---- Additional coverage steps ---- @given( "I create a ProviderCostTable with custom entry for new provider " '"{provider}" model "{model}"' ) def step_create_cost_table_new_provider( context: Context, provider: str, model: str ) -> None: from cleveragents.providers.cost_table import CostEntry, ProviderCostTable custom: dict[str, dict[str, CostEntry]] = { provider: { model: CostEntry(input_cost_per_token=0.005, output_cost_per_token=0.01) } } context.cost_table = ProviderCostTable(custom_entries=custom) @then("listing models for empty provider raises ValueError") def step_list_models_empty_provider(context: Context) -> None: try: context.cost_table.list_models("") raise AssertionError("Expected ValueError") except ValueError: pass @then("estimating cost with negative input tokens raises ValueError on tracker") def step_estimate_negative_input_tracker(context: Context) -> None: try: context.cost_tracker.estimate_cost("openai", "gpt-4o", -1, 0) raise AssertionError("Expected ValueError") except ValueError: pass @then("estimating cost with negative output tokens raises ValueError on tracker") def step_estimate_negative_output_tracker(context: Context) -> None: try: context.cost_tracker.estimate_cost("openai", "gpt-4o", 0, -1) raise AssertionError("Expected ValueError") except ValueError: pass @then("recording tracked usage with negative output raises ValueError") def step_record_tracked_negative_output(context: Context) -> None: try: context.cost_tracker.record_usage( context.cost_metadata, provider="openai", model="gpt-4o", input_tokens=0, output_tokens=-1, ) raise AssertionError("Expected ValueError") except ValueError: pass @then("the budget check should be warning") def step_check_warning(context: Context) -> None: from cleveragents.providers.cost_tracker import BudgetStatus assert context.budget_result.status == BudgetStatus.WARNING, ( f"Expected WARNING, got {context.budget_result.status}" ) @when('I get cost entry for "{provider}" "{model}"') def step_get_cost_entry(context: Context, provider: str, model: str) -> None: context.returned_cost_entry = context.cost_tracker.get_cost_entry(provider, model) @then("the returned cost entry should have positive input cost") def step_check_returned_cost_entry(context: Context) -> None: assert context.returned_cost_entry.input_cost_per_token > 0 @then('the fallback skipped should mention "{reason}"') def step_check_fallback_skipped_reason(context: Context, reason: str) -> None: reasons = [r for _, r in context.fallback_result.skipped] assert any(reason in r for r in reasons), ( f"Expected '{reason}' in skipped reasons: {reasons}" ) @when("I select a fallback provider requiring streaming") def step_select_fallback_streaming(context: Context) -> None: context.fallback_result = context.fallback_selector.select(require_streaming=True) @when("I select a fallback provider requiring vision") def step_select_fallback_vision(context: Context) -> None: context.fallback_result = context.fallback_selector.select(require_vision=True) @when("I select a fallback provider requiring json mode") def step_select_fallback_json_mode(context: Context) -> None: context.fallback_result = context.fallback_selector.select(require_json_mode=True) @then("the fallback result should skip for missing streaming") def step_check_skip_streaming(context: Context) -> None: reasons = [r for _, r in context.fallback_result.skipped] assert len(reasons) > 0, "Expected skipped entries" @then("the fallback result should skip for missing vision") def step_check_skip_vision(context: Context) -> None: reasons = [r for _, r in context.fallback_result.skipped] assert len(reasons) > 0, "Expected skipped entries" @then("the fallback result should skip for missing json mode") def step_check_skip_json_mode(context: Context) -> None: reasons = [r for _, r in context.fallback_result.skipped] assert len(reasons) > 0, "Expected skipped entries"