From b8ab2cb3961628ecea4671515fb4305710af4114 Mon Sep 17 00:00:00 2001 From: HeaTTap Date: Thu, 30 Jul 2026 08:01:21 +0000 Subject: [PATCH 1/3] feat(sdk): Add Claude 3 and Gemini 1.5 Flash to pricing table - Add pricing lookup mappings for claude-3-haiku, claude-3-opus, and gemini-1.5-flash - Support pricing lookup matching for minor and date-pinned versions - Add unit tests verifying cost calculation for new models and date-pinned model names Closes #29 --- packages/sdk/agentscope/_pricing.py | 24 ++++++++++++++++++++++-- packages/sdk/tests/test_sdk.py | 15 +++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/packages/sdk/agentscope/_pricing.py b/packages/sdk/agentscope/_pricing.py index c5e149e..9ebafeb 100644 --- a/packages/sdk/agentscope/_pricing.py +++ b/packages/sdk/agentscope/_pricing.py @@ -6,10 +6,29 @@ "gpt-4": {"input": 30.00, "output": 60.00}, "gpt-3.5-turbo": {"input": 0.50, "output": 1.50}, "claude-3-5-sonnet": {"input": 3.00, "output": 15.00}, + "claude-3-haiku": {"input": 0.25, "output": 1.25}, + "claude-3-opus": {"input": 15.00, "output": 75.00}, "gemini-1.5-pro": {"input": 1.25, "output": 5.00}, + "gemini-1.5-flash": {"input": 0.075, "output": 0.30}, } +def get_model_pricing(model_name: str) -> dict[str, float] | None: + """Lookup pricing configuration for a given model name, including minor/date-pinned versions.""" + if not model_name: + return None + + model_name_lower = model_name.lower() + if model_name_lower in PRICING_TABLE: + return PRICING_TABLE[model_name_lower] + + for key in sorted(PRICING_TABLE.keys(), key=len, reverse=True): + if model_name_lower.startswith(key): + return PRICING_TABLE[key] + + return None + + def calculate_cost( model_name: str, prompt_tokens: int | None, completion_tokens: int | None ) -> float: @@ -23,10 +42,10 @@ def calculate_cost( Returns: The estimated cost in USD (float). """ - if not model_name or model_name not in PRICING_TABLE: + prices = get_model_pricing(model_name) + if not prices: return 0.0 - prices = PRICING_TABLE[model_name] input_tokens = prompt_tokens or 0 output_tokens = completion_tokens or 0 @@ -34,3 +53,4 @@ def calculate_cost( output_cost = (output_tokens / 1_000_000) * prices["output"] return input_cost + output_cost + diff --git a/packages/sdk/tests/test_sdk.py b/packages/sdk/tests/test_sdk.py index 285cfba..15401a5 100644 --- a/packages/sdk/tests/test_sdk.py +++ b/packages/sdk/tests/test_sdk.py @@ -12,6 +12,21 @@ def test_calculate_cost(): assert calculate_cost("gpt-4o", 1_000_000, 1_000_000) == 12.50 assert calculate_cost("unknown-model", 100, 100) == 0.0 + # New model pricing checks + # claude-3-haiku: 0.25 input / 1.25 output per 1M tokens + assert calculate_cost("claude-3-haiku", 1_000_000, 1_000_000) == 1.50 + # claude-3-opus: 15.00 input / 75.00 output per 1M tokens + assert calculate_cost("claude-3-opus", 1_000_000, 1_000_000) == 90.00 + # gemini-1.5-flash: 0.075 input / 0.30 output per 1M tokens + assert calculate_cost("gemini-1.5-flash", 1_000_000, 1_000_000) == pytest.approx(0.375) + + # Date-pinned & minor version matching + assert calculate_cost("claude-3-haiku-20240307", 1_000_000, 1_000_000) == 1.50 + assert calculate_cost("claude-3-opus-20240229", 1_000_000, 1_000_000) == 90.00 + assert calculate_cost("gemini-1.5-flash-001", 1_000_000, 1_000_000) == pytest.approx(0.375) + assert calculate_cost("gpt-4o-2024-05-13", 1_000_000, 1_000_000) == 12.50 + + @trace(name="test_sync") def sync_fn(x): From 1feb892bc6ebecc45995e53e870b268282ca7e2d Mon Sep 17 00:00:00 2001 From: HeaTTap Date: Sat, 1 Aug 2026 15:24:01 +0000 Subject: [PATCH 2/3] fix(sdk): require hyphen delimiter for model prefix matching and reject negative token counts --- packages/sdk/agentscope/_pricing.py | 11 ++++++++--- packages/sdk/tests/test_sdk.py | 9 +++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/sdk/agentscope/_pricing.py b/packages/sdk/agentscope/_pricing.py index 9ebafeb..7f4794c 100644 --- a/packages/sdk/agentscope/_pricing.py +++ b/packages/sdk/agentscope/_pricing.py @@ -23,7 +23,7 @@ def get_model_pricing(model_name: str) -> dict[str, float] | None: return PRICING_TABLE[model_name_lower] for key in sorted(PRICING_TABLE.keys(), key=len, reverse=True): - if model_name_lower.startswith(key): + if model_name_lower.startswith(f"{key}-"): return PRICING_TABLE[key] return None @@ -46,8 +46,13 @@ def calculate_cost( if not prices: return 0.0 - input_tokens = prompt_tokens or 0 - output_tokens = completion_tokens or 0 + if prompt_tokens is not None and prompt_tokens < 0: + raise ValueError("prompt_tokens cannot be negative") + if completion_tokens is not None and completion_tokens < 0: + raise ValueError("completion_tokens cannot be negative") + + input_tokens = 0 if prompt_tokens is None else prompt_tokens + output_tokens = 0 if completion_tokens is None else completion_tokens input_cost = (input_tokens / 1_000_000) * prices["input"] output_cost = (output_tokens / 1_000_000) * prices["output"] diff --git a/packages/sdk/tests/test_sdk.py b/packages/sdk/tests/test_sdk.py index 15401a5..0b84e82 100644 --- a/packages/sdk/tests/test_sdk.py +++ b/packages/sdk/tests/test_sdk.py @@ -26,6 +26,15 @@ def test_calculate_cost(): assert calculate_cost("gemini-1.5-flash-001", 1_000_000, 1_000_000) == pytest.approx(0.375) assert calculate_cost("gpt-4o-2024-05-13", 1_000_000, 1_000_000) == 12.50 + # Ensure non-delimited model names do not false match + assert calculate_cost("gpt-4orange", 1_000_000, 1_000_000) == 0.0 + + # Negative token count validation + with pytest.raises(ValueError, match="prompt_tokens cannot be negative"): + calculate_cost("gpt-4o", -1, 100) + with pytest.raises(ValueError, match="completion_tokens cannot be negative"): + calculate_cost("gpt-4o", 100, -1) + @trace(name="test_sync") From b6f52cbb979d0abc6d21a53c0b81b37379f9fa8d Mon Sep 17 00:00:00 2001 From: HeaTTap Date: Thu, 6 Aug 2026 09:09:31 +0000 Subject: [PATCH 3/3] test(sdk): add uppercase date-pinned and non-delimited model boundary test cases Signed-off-by: HeaTTap --- packages/sdk/tests/test_sdk.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/sdk/tests/test_sdk.py b/packages/sdk/tests/test_sdk.py index 0b84e82..379a5e6 100644 --- a/packages/sdk/tests/test_sdk.py +++ b/packages/sdk/tests/test_sdk.py @@ -22,12 +22,14 @@ def test_calculate_cost(): # Date-pinned & minor version matching assert calculate_cost("claude-3-haiku-20240307", 1_000_000, 1_000_000) == 1.50 + assert calculate_cost("CLAUDE-3-HAIKU-20240307", 1_000_000, 1_000_000) == 1.50 assert calculate_cost("claude-3-opus-20240229", 1_000_000, 1_000_000) == 90.00 assert calculate_cost("gemini-1.5-flash-001", 1_000_000, 1_000_000) == pytest.approx(0.375) assert calculate_cost("gpt-4o-2024-05-13", 1_000_000, 1_000_000) == 12.50 # Ensure non-delimited model names do not false match assert calculate_cost("gpt-4orange", 1_000_000, 1_000_000) == 0.0 + assert calculate_cost("claude-3-haikuish", 1_000_000, 1_000_000) == 0.0 # Negative token count validation with pytest.raises(ValueError, match="prompt_tokens cannot be negative"):