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
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,43 @@ and the project adheres to [Semantic Versioning 2.0.0](https://semver.org/spec/v
> Tags prior to **v0.4.0** were cut in the private repository and produced no
> public artifacts; the first publicly released version is v0.4.0.

## [1.4.1] - 2026-05-30

### Changed
- **`setup` no longer forces an API-key re-entry when one already exists.**
Re-running `commitbrief setup` for a provider that already has a key in the
target config now lets you leave the key prompt blank to keep the stored
key, so switching only the active provider/model doesn't require retyping
credentials. First-time configuration (no existing key) still requires a
non-empty key. For a non-interactive path, `providers use <name>` switches
the active provider and `config set providers.<name>.model <model>` changes
the model — both leave API keys untouched.

### Added
- **OpenAI: GPT-5 family support — `gpt-5.5`, `gpt-5.4-mini`, `gpt-5.5-pro`.**
All three are selectable in `commitbrief setup` and carry correct context
windows (1.05M / 400K / 1.05M input) and pricing. The new default OpenAI
model is **`gpt-5.4-mini`** (was `gpt-4o`); existing configs that pin a
model are unaffected. `gpt-5.5-pro` is served through OpenAI's Responses
API (it is not available on Chat Completions) and may take several minutes
per review. Reasoning models get a larger default output-token budget so a
findings JSON isn't truncated by reasoning tokens.
- **Gemini: `gemini-3.5-flash` support.**

### Changed
- **Gemini model lineup refreshed to the 3.x family.** `gemini-2.5-pro` →
`gemini-3.1-pro-preview`, `gemini-2.5-flash` → `gemini-3.5-flash`,
`gemini-1.5-flash` → `gemini-3.1-flash-lite`. The new default Gemini model
is **`gemini-3.5-flash`** (was `gemini-2.5-pro`). Pricing and context
windows updated; `gemini-3.1-pro-preview` carries tiered pricing (the
≤200K-token base tier is snapshotted). Configs pinning a removed 2.x/1.5
model should switch to a 3.x ID.
- **Anthropic: `claude-opus-4-7` → `claude-opus-4-8`.** Opus 4.8 is now the
default Anthropic model. Its built-in pricing ($5 input / $25 output / $0.50
cache-read per 1M) and context window (1M tokens, up from the 200K the old
table recorded for 4.7) are updated to match Anthropic's current model docs.
Configs pinning `claude-opus-4-7` should switch to `claude-opus-4-8`.

## [1.4.0] - 2026-05-29

### Fixed
Expand Down
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,9 @@ Four API providers + two CLI-tool-backed providers ship in the box:

| Provider | Models | Notes |
|----------|--------|-------|
| **Anthropic** | Claude Opus 4.7, Sonnet 4.6, Haiku 4.5 | Ephemeral prompt caching (5 m TTL) cuts repeated input cost ~10×. |
| **OpenAI** | GPT-4o, GPT-4o-mini | Automatic prompt caching at ≥1024-token prefixes. |
| **Google Gemini** | Gemini 2.5 Pro (2 M context!), 2.5 Flash, 1.5 Flash | Largest free-tier context windows. |
| **Anthropic** | Claude Opus 4.8 (default), Sonnet 4.6, Haiku 4.5 | Ephemeral prompt caching (5 m TTL) cuts repeated input cost ~10×. Opus 4.8 advertises a 1 M-token context. |
| **OpenAI** | GPT-5.4-mini (default), GPT-5.5, GPT-5.5-pro, GPT-4o, GPT-4o-mini | Automatic prompt caching at ≥1024-token prefixes. `gpt-5.5-pro` runs via the Responses API (not Chat Completions) and can take several minutes per review. |
| **Google Gemini** | Gemini 3.5 Flash (default), 3.1 Pro, 3.1 Flash-Lite | ~1 M-token context windows. `gemini-3.1-pro-preview` is a preview model. |
| **DeepSeek** | deepseek-chat, deepseek-reasoner | OpenAI-compatible API (`DEEPSEEK_API_KEY`); JSON is prompt-driven (degrades gracefully). |
| **Mistral** | Mistral Large / Small, Codestral | OpenAI-compatible API (`MISTRAL_API_KEY`). |
| **Cohere** | Command R+ / R, Command A | Cohere's OpenAI-compatibility endpoint (`COHERE_API_KEY`). |
Expand Down Expand Up @@ -361,7 +361,7 @@ Two-tier YAML config with field-level merge:

Plus environment variables for credentials and runtime tweaks, and
CLI flags for one-off overrides (`--provider gemini --model
gemini-2.5-flash`).
gemini-3.5-flash`).

| Variable | Effect |
|---|---|
Expand All @@ -379,13 +379,13 @@ version: 1
provider: anthropic # default provider
providers:
anthropic:
model: claude-opus-4-7
model: claude-opus-4-8
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
claude-opus-4-8: # (cost preflight / verbose footer / cache)
input_per_1m: 5.0
output_per_1m: 25.0 # omitted fields keep the built-in value
openai:
model: gpt-4o
model: gpt-5.4-mini
ollama:
model: qwen2.5-coder:14b
base_url: http://localhost:11434
Expand Down
6 changes: 3 additions & 3 deletions internal/config/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ func Default() *Config {
Version: CurrentSchemaVersion,
Provider: "anthropic",
Providers: map[string]ProviderConfig{
"anthropic": {Model: "claude-opus-4-7", BaseURL: "https://api.anthropic.com"},
"openai": {Model: "gpt-4o", BaseURL: "https://api.openai.com/v1"},
"gemini": {Model: "gemini-2.5-pro"},
"anthropic": {Model: "claude-opus-4-8", BaseURL: "https://api.anthropic.com"},
"openai": {Model: "gpt-5.4-mini", BaseURL: "https://api.openai.com/v1"},
"gemini": {Model: "gemini-3.5-flash"},
"ollama": {Model: "qwen2.5-coder:14b", BaseURL: "http://localhost:11434"},
},
Output: OutputConfig{
Expand Down
2 changes: 2 additions & 0 deletions internal/i18n/messages.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ setup.provider.prompt: "Which provider would you like to configure?"
setup.provider.help: "Pick the LLM provider you have an API key for."
setup.api_key.prompt: "Enter your API key:"
setup.api_key.empty: "API key cannot be empty."
setup.api_key.prompt_keep: "Enter a new API key (leave blank to keep the existing one):"
setup.api_key.help_keep: "A key is already configured for this provider. Leave blank to keep it, or enter a new one to replace it."
setup.base_url.prompt: "Ollama base URL:"
setup.model.prompt: "Pick a model:"
setup.model.discover_failed: "Model name (could not discover from Ollama):"
Expand Down
2 changes: 2 additions & 0 deletions internal/i18n/messages.tr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ setup.provider.prompt: "Hangi sağlayıcıyı yapılandırmak istersiniz?"
setup.provider.help: "API anahtarınız olan LLM sağlayıcısını seçin."
setup.api_key.prompt: "API anahtarınızı girin:"
setup.api_key.empty: "API anahtarı boş olamaz."
setup.api_key.prompt_keep: "Yeni bir API anahtarı girin (mevcut anahtarı korumak için boş bırakın):"
setup.api_key.help_keep: "Bu sağlayıcı için zaten bir anahtar yapılandırılmış. Korumak için boş bırakın ya da değiştirmek için yenisini girin."
setup.base_url.prompt: "Ollama temel URL'si:"
setup.model.prompt: "Bir model seçin:"
setup.model.discover_failed: "Model adı (Ollama'dan keşfedilemedi):"
Expand Down
18 changes: 9 additions & 9 deletions internal/provider/anthropic/anthropic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestModelsList(t *testing.T) {
t.Errorf("Models() length = %d, want 3", len(got))
}
want := map[string]bool{
ModelOpus47: true,
ModelOpus48: true,
ModelSonnet46: true,
ModelHaiku45: true,
}
Expand All @@ -41,8 +41,8 @@ func TestModelsDefensiveCopy(t *testing.T) {
}

func TestIsModelSupported(t *testing.T) {
if !IsModelSupported(ModelOpus47) {
t.Error("Opus 4.7 should be supported")
if !IsModelSupported(ModelOpus48) {
t.Error("Opus 4.8 should be supported")
}
if IsModelSupported("gpt-4o") {
t.Error("OpenAI model should not be reported as supported")
Expand All @@ -53,8 +53,8 @@ func TestIsModelSupported(t *testing.T) {
}

func TestContextWindow(t *testing.T) {
if contextWindowFor(ModelOpus47) != 200_000 {
t.Error("Opus 4.7 context window wrong")
if contextWindowFor(ModelOpus48) != 1_000_000 {
t.Error("Opus 4.8 should advertise the 1M window")
}
if contextWindowFor(ModelSonnet46) != 1_000_000 {
t.Error("Sonnet 4.6 should advertise the 1M window")
Expand All @@ -65,7 +65,7 @@ func TestContextWindow(t *testing.T) {
}

func TestPricingLookup(t *testing.T) {
p := pricingFor(ModelOpus47)
p := pricingFor(ModelOpus48)
if p.InputPer1M == 0 || p.OutputPer1M == 0 {
t.Errorf("Opus pricing missing: %+v", p)
}
Expand Down Expand Up @@ -223,7 +223,7 @@ func messagePayload(content []map[string]any, inputTokens, outputTokens int) map
"id": "msg_test",
"type": "message",
"role": "assistant",
"model": ModelOpus47,
"model": ModelOpus48,
"content": content,
"stop_reason": "end_turn",
"stop_sequence": nil,
Expand Down Expand Up @@ -253,7 +253,7 @@ func TestReviewWithFakeServerDegradesToText(t *testing.T) {
t.Fatal(err)
}
resp, err := c.Review(context.Background(), provider.Request{
Model: ModelOpus47,
Model: ModelOpus48,
SystemPrompt: "rules",
UserPrompt: "diff",
MaxTokens: 256,
Expand Down Expand Up @@ -291,7 +291,7 @@ func TestReviewWithToolUseFakeServer(t *testing.T) {
t.Fatal(err)
}
resp, err := c.Review(context.Background(), provider.Request{
Model: ModelOpus47,
Model: ModelOpus48,
SystemPrompt: "rules",
UserPrompt: "diff",
MaxTokens: 256,
Expand Down
2 changes: 1 addition & 1 deletion internal/provider/anthropic/context_window.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ package anthropic
const defaultContextWindow = 200_000

var contextWindows = map[string]int{
ModelOpus47: 200_000,
ModelOpus48: 1_000_000,
ModelSonnet46: 1_000_000,
ModelHaiku45: 200_000,
}
Expand Down
6 changes: 3 additions & 3 deletions internal/provider/anthropic/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ package anthropic
const (
Name = "anthropic"

ModelOpus47 = "claude-opus-4-7"
ModelOpus48 = "claude-opus-4-8"
ModelSonnet46 = "claude-sonnet-4-6"
ModelHaiku45 = "claude-haiku-4-5-20251001"

DefaultModel = ModelOpus47
DefaultModel = ModelOpus48
)

var supportedModels = []string{
ModelOpus47,
ModelOpus48,
ModelSonnet46,
ModelHaiku45,
}
Expand Down
8 changes: 4 additions & 4 deletions internal/provider/anthropic/pricing.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import "github.com/CommitBrief/commitbrief/internal/provider"
// should be refreshed when a model price changes. Cached input is the
// "prompt cache read" rate per Anthropic's ephemeral cache discount.
var pricingTable = map[string]provider.Pricing{
ModelOpus47: {
InputPer1M: 15.00,
OutputPer1M: 75.00,
CachedInputPer1M: 1.50,
ModelOpus48: {
InputPer1M: 5.00,
OutputPer1M: 25.00,
CachedInputPer1M: 0.50,
},
ModelSonnet46: {
InputPer1M: 3.00,
Expand Down
10 changes: 7 additions & 3 deletions internal/provider/gemini/context_window.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ package gemini

const defaultContextWindow = 1_000_000

// Gemini 3.x family baseline input window is 1M tokens. Exact published
// limits for these preview models were not yet broken out in the model
// docs at integration time; 1M is the documented floor and the safe value
// for the over-context guard. Bump if Google publishes larger windows.
var contextWindows = map[string]int{
ModelPro2_5: 2_000_000,
ModelFlash2_5: 1_000_000,
ModelFlash1_5: 1_000_000,
ModelPro31: 1_000_000,
ModelFlash35: 1_000_000,
ModelFlashLite31: 1_000_000,
}

func contextWindowFor(model string) int {
Expand Down
12 changes: 6 additions & 6 deletions internal/provider/gemini/gemini_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,25 @@ func TestModelsDefensiveCopy(t *testing.T) {
}

func TestIsModelSupported(t *testing.T) {
if !IsModelSupported(ModelPro2_5) {
t.Error("gemini-2.5-pro should be supported")
if !IsModelSupported(ModelPro31) {
t.Error("gemini-3.1-pro-preview should be supported")
}
if IsModelSupported("gpt-4o") {
t.Error("OpenAI model should not be supported here")
}
}

func TestContextWindow(t *testing.T) {
if contextWindowFor(ModelPro2_5) != 2_000_000 {
t.Errorf("pro 2.5 context window wrong: %d", contextWindowFor(ModelPro2_5))
if contextWindowFor(ModelPro31) != 1_000_000 {
t.Errorf("pro 3.1 context window wrong: %d", contextWindowFor(ModelPro31))
}
if contextWindowFor("unknown") != defaultContextWindow {
t.Error("unknown model should fall back to defaultContextWindow")
}
}

func TestPricingLookup(t *testing.T) {
p := pricingFor(ModelPro2_5)
p := pricingFor(ModelPro31)
if p.InputPer1M == 0 || p.OutputPer1M == 0 {
t.Errorf("pro pricing missing: %+v", p)
}
Expand Down Expand Up @@ -138,7 +138,7 @@ func TestReviewWithFakeServer(t *testing.T) {
t.Fatal(err)
}
resp, err := c.Review(context.Background(), provider.Request{
Model: ModelPro2_5,
Model: ModelPro31,
SystemPrompt: "rules",
UserPrompt: "diff",
MaxTokens: 256,
Expand Down
17 changes: 10 additions & 7 deletions internal/provider/gemini/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ package gemini
const (
Name = "gemini"

ModelPro2_5 = "gemini-2.5-pro"
ModelFlash2_5 = "gemini-2.5-flash"
ModelFlash1_5 = "gemini-1.5-flash"
// ModelPro31 is a preview model: the public pricing page lists the Pro
// tier only as gemini-3.1-pro-preview. Update to the stable ID once
// Google promotes it out of preview.
ModelPro31 = "gemini-3.1-pro-preview"
ModelFlash35 = "gemini-3.5-flash"
ModelFlashLite31 = "gemini-3.1-flash-lite"

DefaultModel = ModelPro2_5
DefaultModel = ModelFlash35
)

var supportedModels = []string{
ModelPro2_5,
ModelFlash2_5,
ModelFlash1_5,
ModelPro31,
ModelFlash35,
ModelFlashLite31,
}

func Models() []string {
Expand Down
34 changes: 18 additions & 16 deletions internal/provider/gemini/pricing.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,28 @@ package gemini

import "github.com/CommitBrief/commitbrief/internal/provider"

// Gemini per-1M-token pricing snapshot (paid tier).
// Gemini per-1M-token pricing snapshot (paid "Standard" tier).
// Source: https://ai.google.dev/gemini-api/docs/pricing
// CachedInputPer1M reflects Gemini's context-caching discount (separate
// API; we track the per-token rate so cost reporting is accurate when a
// cache is wired up in a future phase).
// gemini-3.1-pro-preview has tiered pricing (≤200K vs >200K input tokens:
// $2/$12 vs $4/$18); we snapshot the ≤200K base, so the verbose cost footer
// may under-report on very large inputs. CachedInputPer1M reflects Gemini's
// implicit-cache read discount (~0.25× input); context caching is not wired
// into the client yet, so the rate is informational for now.
var pricingTable = map[string]provider.Pricing{
ModelPro2_5: {
InputPer1M: 1.25,
OutputPer1M: 10.00,
CachedInputPer1M: 0.31,
ModelPro31: {
InputPer1M: 2.00,
OutputPer1M: 12.00,
CachedInputPer1M: 0.50,
},
ModelFlash2_5: {
InputPer1M: 0.30,
OutputPer1M: 2.50,
CachedInputPer1M: 0.075,
ModelFlash35: {
InputPer1M: 1.50,
OutputPer1M: 9.00,
CachedInputPer1M: 0.375,
},
ModelFlash1_5: {
InputPer1M: 0.075,
OutputPer1M: 0.30,
CachedInputPer1M: 0.01875,
ModelFlashLite31: {
InputPer1M: 0.25,
OutputPer1M: 1.50,
CachedInputPer1M: 0.0625,
},
}

Expand Down
Loading
Loading