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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ and the project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/v
comments is now derived from the PR's `url` field (which always points
at the base repo, including cross-fork PRs).
### Added
- **Per-model pricing override (OQ-09).** `providers.<name>.pricing.<model>`
in config overrides the built-in `$/1M`-token rate snapshot used by the
cost preflight, the verbose footer, and cached-cost figures. Fields:
`input_per_1m`, `output_per_1m`, `cached_input_per_1m`; zero/omitted
fields fall back to the built-in value (partial override OK). Edited in
the config file and shown by `commitbrief config show`. Useful when the
hard-coded snapshot drifts or for a negotiated rate.
- **CI integration: the `commitbrief-action` GitHub Action.** A separate
repo (`CommitBrief/commitbrief-action`) ships a composite action that
runs CommitBrief on pull requests — either posting inline review
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,10 @@ provider: anthropic # default provider
providers:
anthropic:
model: claude-opus-4-7
pricing: # optional: override built-in $/1M rates
claude-opus-4-7: # (cost preflight / verbose footer / cache)
input_per_1m: 15.0
output_per_1m: 75.0 # omitted fields keep the built-in value
openai:
model: gpt-4o
ollama:
Expand Down
2 changes: 1 addition & 1 deletion internal/cli/compress.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func newCompressCmd() *cobra.Command {
latency := time.Since(start)

percent, deltaTokens := result.Savings()
pricing := prov.Pricing(model)
pricing := resolvePricing(app.Config, prov, model)
// Cost-per-review input savings: input-token rate * delta tokens.
perReviewSavedUSD := float64(deltaTokens) * pricing.InputPer1M / 1_000_000

Expand Down
2 changes: 1 addition & 1 deletion internal/cli/dryrun.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func newDryRunCmd() *cobra.Command {
modelName = prov.DefaultModel()
}
contextWindow = prov.ContextWindow(modelName)
estCost = prov.Pricing(modelName).Cost(provider.Usage{
estCost = resolvePricing(app.Config, prov, modelName).Cost(provider.Usage{
InputTokens: inputTokens,
OutputTokens: outputTokens,
})
Expand Down
40 changes: 40 additions & 0 deletions internal/cli/pricing.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package cli

import (
"github.com/CommitBrief/commitbrief/internal/config"
"github.com/CommitBrief/commitbrief/internal/provider"
)

// resolvePricing returns the effective per-model pricing: the provider's
// built-in rate table, with any user override from
// `providers.<active>.pricing.<model>` merged on top (OQ-09). Only
// non-zero override fields apply, so a partial override (e.g. just
// output_per_1m) keeps the built-in value for the rest. Used everywhere a
// dollar figure is computed (cost preflight, verbose footer, cached cost,
// dry-run, compress savings) so the override is honored uniformly.
func resolvePricing(cfg *config.Config, prov provider.Provider, model string) provider.Pricing {
base := prov.Pricing(model)
if cfg == nil {
return base
}
pc, ok := cfg.Providers[cfg.Provider]
if !ok {
return base
}
mp, ok := pc.Pricing[model]
if !ok {
return base
}
if mp.InputPer1M != 0 {
base.InputPer1M = mp.InputPer1M
}
if mp.OutputPer1M != 0 {
base.OutputPer1M = mp.OutputPer1M
}
if mp.CachedInputPer1M != 0 {
base.CachedInputPer1M = mp.CachedInputPer1M
}
return base
}
57 changes: 57 additions & 0 deletions internal/cli/pricing_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package cli

import (
"testing"

"github.com/CommitBrief/commitbrief/internal/config"
"github.com/CommitBrief/commitbrief/internal/provider"
"github.com/CommitBrief/commitbrief/internal/provider/mock"
)

func TestResolvePricing(t *testing.T) {
const model = "mock-model"
base := provider.Pricing{InputPer1M: 1.0, OutputPer1M: 2.0, CachedInputPer1M: 0.5}
prov := mock.New()
prov.PricingValue = base

withOverride := func(mp config.ModelPricing) *config.Config {
return &config.Config{
Provider: "mock",
Providers: map[string]config.ProviderConfig{"mock": {Pricing: map[string]config.ModelPricing{model: mp}}},
}
}

// nil cfg → built-in.
if got := resolvePricing(nil, prov, model); got != base {
t.Errorf("nil cfg should yield built-in, got %+v", got)
}

// No pricing map → built-in.
cfgNone := &config.Config{Provider: "mock", Providers: map[string]config.ProviderConfig{"mock": {}}}
if got := resolvePricing(cfgNone, prov, model); got != base {
t.Errorf("no override should yield built-in, got %+v", got)
}

// Full override replaces all three.
got := resolvePricing(withOverride(config.ModelPricing{InputPer1M: 9, OutputPer1M: 8, CachedInputPer1M: 7}), prov, model)
if got.InputPer1M != 9 || got.OutputPer1M != 8 || got.CachedInputPer1M != 7 {
t.Errorf("full override = %+v, want {9,8,7}", got)
}

// Partial override (only output) keeps built-in for the rest.
got = resolvePricing(withOverride(config.ModelPricing{OutputPer1M: 99}), prov, model)
if got.InputPer1M != 1.0 || got.OutputPer1M != 99 || got.CachedInputPer1M != 0.5 {
t.Errorf("partial override = %+v, want {1.0, 99, 0.5}", got)
}

// Override for a different model → no effect on this model.
cfgOther := &config.Config{
Provider: "mock",
Providers: map[string]config.ProviderConfig{"mock": {Pricing: map[string]config.ModelPricing{"other": {InputPer1M: 50}}}},
}
if got := resolvePricing(cfgOther, prov, model); got != base {
t.Errorf("override for a different model should not apply, got %+v", got)
}
}
6 changes: 3 additions & 3 deletions internal/cli/review.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ func runReview(cmd *cobra.Command, scope reviewScopeFlags, diffArgs []string) er
Cached: true,
Timestamp: entry.CreatedAt,
Usage: usage,
Cost: prov.Pricing(model).Cost(usage),
Cost: resolvePricing(app.Config, prov, model).Cost(usage),
Files: parsed.FileCount(),
LinesAdded: parsed.AddedLines(),
LinesRemoved: parsed.DeletedLines(),
Expand Down Expand Up @@ -284,7 +284,7 @@ func runReview(cmd *cobra.Command, scope reviewScopeFlags, diffArgs []string) er
InputTokens: p.EstimatedTokens(),
OutputTokens: estimateOutputTokens(p.EstimatedTokens()),
}
estCost := prov.Pricing(model).Cost(estUsage)
estCost := resolvePricing(app.Config, prov, model).Cost(estUsage)
prog.Pause()
if abort := handleCostPreflight(cmd, app, estCost, stdinReader); abort {
return errors.New(app.Catalog.T("cost.aborted_user"))
Expand Down Expand Up @@ -356,7 +356,7 @@ func runReview(cmd *cobra.Command, scope reviewScopeFlags, diffArgs []string) er
Model: respModel,
Lang: app.Lang.Code,
Usage: usage,
Cost: prov.Pricing(respModel).Cost(usage),
Cost: resolvePricing(app.Config, prov, respModel).Cost(usage),
Latency: latency,
Timestamp: time.Now().UTC(),
Files: parsed.FileCount(),
Expand Down
15 changes: 15 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,21 @@ type ProviderConfig struct {
APIKey string `yaml:"api_key,omitempty"`
Model string `yaml:"model,omitempty"`
BaseURL string `yaml:"base_url,omitempty"`

// Pricing overrides the built-in per-model rate table (OQ-09), keyed
// by model name. Useful when the hard-coded snapshot drifts or for a
// negotiated rate. Zero fields fall back to the built-in value, so a
// partial override (e.g. only output_per_1m) is allowed. Consumed by
// the cost preflight, verbose footer, and cached-cost figures via
// resolvePricing (internal/cli).
Pricing map[string]ModelPricing `yaml:"pricing,omitempty"`
}

// ModelPricing is a per-1M-token rate override for one model.
type ModelPricing struct {
InputPer1M float64 `yaml:"input_per_1m,omitempty"`
OutputPer1M float64 `yaml:"output_per_1m,omitempty"`
CachedInputPer1M float64 `yaml:"cached_input_per_1m,omitempty"`
}

type OutputConfig struct {
Expand Down
Loading