Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Missing pricing for current models costed them at $0** — several current
models had no `DEFAULT_PRICING` entry, so `get_pricing` returned `None` and
they were silently costed at $0 in the Token Usage Summary / cost breakdown
(dotted version suffixes like `claude-opus-4.8` are not bridged by the
`-`-delimited fuzzy fallback). Added entries for `claude-opus-4.7`,
`claude-opus-4.8`, `claude-sonnet-5`, `gpt-5.3-codex`, `gpt-5.4`, `gpt-5.5`,
`gpt-5-mini`, `gpt-5.4-mini`, and `gemini-3.5-flash`. (The related
sub-workflow usage under-reporting in the same issue was already fixed in
[#212](https://github.com/microsoft/conductor/pull/212).)
([#266](https://github.com/microsoft/conductor/issues/266))
- **For-each dive-in worked only for finished items** — in the web dashboard's
for-each group detail panel, the per-item "Dive into subworkflow" control was
nested inside the row's expand/collapse `<button>`, which is `disabled` while
Expand Down
32 changes: 31 additions & 1 deletion src/conductor/engine/pricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,27 @@ class ModelPricing:
"gpt-4.1-mini": ModelPricing(input_per_mtok=0.15, output_per_mtok=0.60),
"gpt-4": ModelPricing(input_per_mtok=30.00, output_per_mtok=60.00),
"gpt-3.5-turbo": ModelPricing(input_per_mtok=0.50, output_per_mtok=1.50),
# GPT-5.x Series (uniform standard rate; mini variants at the mini tier)
"gpt-5.5": ModelPricing(input_per_mtok=2.00, output_per_mtok=8.00),
"gpt-5.4": ModelPricing(input_per_mtok=2.00, output_per_mtok=8.00),
"gpt-5.3-codex": ModelPricing(input_per_mtok=2.00, output_per_mtok=8.00),
"gpt-5.2": ModelPricing(input_per_mtok=2.00, output_per_mtok=8.00),
"gpt-5.1": ModelPricing(input_per_mtok=2.00, output_per_mtok=8.00),
"gpt-5.4-mini": ModelPricing(input_per_mtok=0.15, output_per_mtok=0.60),
"gpt-5-mini": ModelPricing(input_per_mtok=0.15, output_per_mtok=0.60),
# O-series
"o1": ModelPricing(input_per_mtok=15.00, output_per_mtok=60.00),
"o1-mini": ModelPricing(input_per_mtok=3.00, output_per_mtok=12.00),
"o1-preview": ModelPricing(input_per_mtok=15.00, output_per_mtok=60.00),
"o3-mini": ModelPricing(input_per_mtok=1.10, output_per_mtok=4.40),
# Claude 4.5 Series (newest)
# Claude 5 Series (newest)
"claude-sonnet-5": ModelPricing(
input_per_mtok=3.00,
output_per_mtok=15.00,
cache_read_per_mtok=0.30,
cache_write_per_mtok=3.75,
),
# Claude 4.5 Series
"claude-opus-4-5": ModelPricing(
input_per_mtok=5.00,
output_per_mtok=25.00,
Expand Down Expand Up @@ -133,6 +146,19 @@ class ModelPricing:
cache_read_per_mtok=0.30,
cache_write_per_mtok=3.75,
),
# Claude 4.7 / 4.8 Series
"claude-opus-4.8": ModelPricing(
input_per_mtok=5.00,
output_per_mtok=25.00,
cache_read_per_mtok=0.50,
cache_write_per_mtok=6.25,
),
"claude-opus-4.7": ModelPricing(
input_per_mtok=5.00,
output_per_mtok=25.00,
cache_read_per_mtok=0.50,
cache_write_per_mtok=6.25,
),
# Claude 4 Series
"claude-opus-4": ModelPricing(
input_per_mtok=15.00,
Expand Down Expand Up @@ -212,6 +238,10 @@ class ModelPricing:
input_per_mtok=1.25,
output_per_mtok=5.00,
),
"gemini-3.5-flash": ModelPricing(
input_per_mtok=0.30,
output_per_mtok=2.50,
),
}


Expand Down
50 changes: 42 additions & 8 deletions tests/test_engine/test_pricing.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,37 @@ def test_default_pricing_table_has_expected_models(self) -> None:
for model in expected_models:
assert model in DEFAULT_PRICING, f"Expected {model} in DEFAULT_PRICING"

@pytest.mark.parametrize(
("model", "input_per_mtok", "output_per_mtok"),
[
# GPT-5.x family (standard rate) and mini variants (#266).
("gpt-5.5", 2.00, 8.00),
("gpt-5.4", 2.00, 8.00),
("gpt-5.3-codex", 2.00, 8.00),
("gpt-5-mini", 0.15, 0.60),
("gpt-5.4-mini", 0.15, 0.60),
# Claude families (#266).
("claude-opus-4.8", 5.00, 25.00),
("claude-opus-4.7", 5.00, 25.00),
("claude-sonnet-5", 3.00, 15.00),
# Gemini (#266).
("gemini-3.5-flash", 0.30, 2.50),
],
)
def test_current_models_have_exact_pricing(
self, model: str, input_per_mtok: float, output_per_mtok: float
) -> None:
"""Current models resolve via an exact key, not the $0 None fallback (#266).

These dotted version suffixes are not bridged by the ``-``-delimited
fuzzy fallback, so a missing exact key silently costs $0.
"""
assert model in DEFAULT_PRICING, f"Expected {model} in DEFAULT_PRICING"
pricing = get_pricing(model)
assert pricing is not None, f"{model} unexpectedly returned None pricing"
assert pricing.input_per_mtok == input_per_mtok
assert pricing.output_per_mtok == output_per_mtok


class TestFuzzyMatchWarnings:
"""Tests for the fuzzy-match warning behavior (#137)."""
Expand Down Expand Up @@ -141,15 +172,18 @@ def test_unknown_model_does_not_warn(self, caplog: pytest.LogCaptureFixture) ->
assert caplog.records == []

def test_cross_family_name_returns_none_no_warn(self, caplog: pytest.LogCaptureFixture) -> None:
# Repro from #137: these names share a textual prefix with claude-opus-4
# but are different model families. The delimiter check should reject
# the match entirely (returning None and degrading gracefully) rather
# than silently inheriting claude-opus-4's pricing/context-window.
# Repro from #137: a dotted version suffix shares a textual prefix with
# the dashed base key claude-opus-4 but is a different family. The
# delimiter check must reject the match entirely (returning None and
# degrading gracefully) rather than silently inheriting claude-opus-4's
# pricing/context-window. A deliberately synthetic version (4.123) is
# used so this stays absent from DEFAULT_PRICING as real dotted models
# (4.6/4.7/4.8/…) get added over time.
with caplog.at_level("WARNING", logger="conductor.engine.pricing"):
assert get_pricing("claude-opus-4.7") is None
assert get_pricing("claude-opus-4.7-high") is None
assert get_pricing("claude-opus-4.7-xhigh") is None
assert get_pricing("claude-opus-4.7-1m-internal") is None
assert get_pricing("claude-opus-4.123") is None
assert get_pricing("claude-opus-4.123-high") is None
assert get_pricing("claude-opus-4.123-xhigh") is None
assert get_pricing("claude-opus-4.123-1m-internal") is None
assert caplog.records == []

def test_versioned_suffix_match_warns(self, caplog: pytest.LogCaptureFixture) -> None:
Expand Down
Loading