fs: show OpenRouter cache pricing for selected models
This commit is contained in:
parent
3ca84487c5
commit
2e30b0ddab
|
|
@ -31,6 +31,8 @@ type openAIModelInfo struct {
|
|||
Pricing *struct {
|
||||
Prompt string `json:"prompt"`
|
||||
Completion string `json:"completion"`
|
||||
InputCacheRead string `json:"input_cache_read"`
|
||||
InputCacheWrite string `json:"input_cache_write"`
|
||||
} `json:"pricing,omitempty"`
|
||||
}
|
||||
|
||||
|
|
@ -124,7 +126,13 @@ func (b *OpenAIBackend) ModelsInfo(ctx context.Context) []ModelInfo {
|
|||
if v, err := strconv.ParseFloat(m.Pricing.Completion, 64); err == nil {
|
||||
pricing.Output = v
|
||||
}
|
||||
if pricing.Input > 0 || pricing.Output > 0 {
|
||||
if v, err := strconv.ParseFloat(m.Pricing.InputCacheRead, 64); err == nil {
|
||||
pricing.CacheRead = v
|
||||
}
|
||||
if v, err := strconv.ParseFloat(m.Pricing.InputCacheWrite, 64); err == nil {
|
||||
pricing.CacheWrite = v
|
||||
}
|
||||
if pricing.Input > 0 || pricing.Output > 0 || pricing.CacheRead > 0 || pricing.CacheWrite > 0 {
|
||||
info[i].Pricing = &pricing
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -507,6 +507,8 @@ func TestModelsInfo_ParsesPricing(t *testing.T) {
|
|||
"pricing": map[string]string{
|
||||
"prompt": "0.000003",
|
||||
"completion": "0.000015",
|
||||
"input_cache_read": "0.0000003",
|
||||
"input_cache_write": "0.00000375",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
@ -541,6 +543,12 @@ func TestModelsInfo_ParsesPricing(t *testing.T) {
|
|||
if info[0].Pricing.Output != 0.000015 {
|
||||
t.Errorf("Output = %v, want 0.000015", info[0].Pricing.Output)
|
||||
}
|
||||
if info[0].Pricing.CacheRead != 0.0000003 {
|
||||
t.Errorf("CacheRead = %v, want 0.0000003", info[0].Pricing.CacheRead)
|
||||
}
|
||||
if info[0].Pricing.CacheWrite != 0.00000375 {
|
||||
t.Errorf("CacheWrite = %v, want 0.00000375", info[0].Pricing.CacheWrite)
|
||||
}
|
||||
|
||||
// Second model has no pricing
|
||||
if info[1].ID != "no-pricing-model" {
|
||||
|
|
|
|||
|
|
@ -89,6 +89,39 @@ func writeModelLine(sb *strings.Builder, backendName, modelID string, pricing *b
|
|||
sb.WriteByte('\n')
|
||||
}
|
||||
|
||||
// modelPricingBlock renders a model name followed by its per-million-token
|
||||
// USD pricing, in the format returned by the agent ctl "model" command.
|
||||
// Resolution mirrors ModelCache.refresh: API-provided pricing (ModelLister)
|
||||
// first, then the static estimate table. Returns name-only when no pricing
|
||||
// is known.
|
||||
func modelPricingBlock(be backend.Backend, modelID string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString(modelID)
|
||||
sb.WriteByte('\n')
|
||||
|
||||
var pricing *backend.ModelPricing
|
||||
if lister, ok := be.(backend.ModelLister); ok {
|
||||
for _, m := range lister.ModelsInfo(context.Background()) {
|
||||
if m.ID == modelID {
|
||||
pricing = m.Pricing
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if pricing == nil {
|
||||
pricing = backend.LookupStaticPricing(modelID)
|
||||
}
|
||||
if pricing == nil {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
fmt.Fprintf(&sb, "In/Mtok: %s\n", formatPricingWithEstimate(pricing.Input, pricing.Estimated))
|
||||
fmt.Fprintf(&sb, "Out/Mtok: %s\n", formatPricingWithEstimate(pricing.Output, pricing.Estimated))
|
||||
fmt.Fprintf(&sb, "CacheRead/Mtok: %s\n", formatPricingWithEstimate(pricing.CacheRead, pricing.Estimated))
|
||||
fmt.Fprintf(&sb, "CacheWrite/Mtok: %s\n", formatPricingWithEstimate(pricing.CacheWrite, pricing.Estimated))
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatPricingWithEstimate formats per-token cost as per-million-tokens USD string.
|
||||
// Returns "-" if the value is zero. Appends "(e)" if estimated is true and value is non-zero.
|
||||
func formatPricingWithEstimate(perToken float64, estimated bool) string {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,90 @@
|
|||
package fs
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"ollie/cmd/olliesrv/internal/backend"
|
||||
)
|
||||
|
||||
// stubCtlBackend satisfies backend.Backend for pricing-block tests.
|
||||
type stubCtlBackend struct{}
|
||||
|
||||
func (stubCtlBackend) ChatStream(context.Context, []backend.Message, []backend.Tool, backend.GenerationParams) (<-chan backend.StreamEvent, error) {
|
||||
return nil, fmt.Errorf("not implemented")
|
||||
}
|
||||
func (stubCtlBackend) Name() string { return "stub" }
|
||||
func (stubCtlBackend) DefaultModel() string { return "stub-model" }
|
||||
func (stubCtlBackend) Model() string { return "stub-model" }
|
||||
func (stubCtlBackend) SetModel(string) {}
|
||||
func (stubCtlBackend) ContextLength(context.Context) int { return 0 }
|
||||
func (stubCtlBackend) Models(context.Context) []string { return nil }
|
||||
|
||||
// stubCtlLister adds API-provided pricing for one exact model ID.
|
||||
type stubCtlLister struct{ stubCtlBackend }
|
||||
|
||||
func (stubCtlLister) ModelsInfo(context.Context) []backend.ModelInfo {
|
||||
return []backend.ModelInfo{
|
||||
{ID: "exact-model", Pricing: &backend.ModelPricing{
|
||||
Input: 2.5 / 1_000_000, Output: 10.0 / 1_000_000, CacheRead: 1.25 / 1_000_000,
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelPricingBlock(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
be backend.Backend
|
||||
id string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "api_pricing_exact_match",
|
||||
be: stubCtlLister{},
|
||||
id: "exact-model",
|
||||
want: "exact-model\n" +
|
||||
"In/Mtok: $2.50\n" +
|
||||
"Out/Mtok: $10.00\n" +
|
||||
"CacheRead/Mtok: $1.25\n" +
|
||||
"CacheWrite/Mtok: -\n",
|
||||
},
|
||||
{
|
||||
name: "api_pricing_miss_falls_back_to_static",
|
||||
be: stubCtlLister{},
|
||||
id: "claude-sonnet-4-5",
|
||||
want: "claude-sonnet-4-5\n" +
|
||||
"In/Mtok: $3.00(e)\n" +
|
||||
"Out/Mtok: $15.00(e)\n" +
|
||||
"CacheRead/Mtok: $0.3000(e)\n" +
|
||||
"CacheWrite/Mtok: $3.75(e)\n",
|
||||
},
|
||||
{
|
||||
name: "static_pricing_no_lister",
|
||||
be: stubCtlBackend{},
|
||||
id: "gpt-4o",
|
||||
want: "gpt-4o\n" +
|
||||
"In/Mtok: $2.50(e)\n" +
|
||||
"Out/Mtok: $10.00(e)\n" +
|
||||
"CacheRead/Mtok: $1.25(e)\n" +
|
||||
"CacheWrite/Mtok: -\n",
|
||||
},
|
||||
{
|
||||
name: "no_pricing_known",
|
||||
be: stubCtlBackend{},
|
||||
id: "totally-unknown-model",
|
||||
want: "totally-unknown-model\n",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := modelPricingBlock(tt.be, tt.id)
|
||||
if got != tt.want {
|
||||
t.Errorf("modelPricingBlock(%q) =\n%q\nwant\n%q", tt.id, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatPricingWithEstimate(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
|
|
|||
|
|
@ -1000,10 +1000,12 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
|||
}
|
||||
return nil, nil
|
||||
}
|
||||
name := strings.Join(args, " ")
|
||||
if be := a.Backend(); be != nil {
|
||||
be.SetModel(strings.Join(args, " "))
|
||||
be.SetModel(name)
|
||||
return []byte(modelPricingBlock(be, name)), nil
|
||||
}
|
||||
return []byte(strings.Join(args, " ") + "\n"), nil
|
||||
return []byte(name + "\n"), nil
|
||||
},
|
||||
"models": func(_ []string) ([]byte, error) {
|
||||
return []byte(s.CachedListModels() + "\n"), nil
|
||||
|
|
|
|||
Loading…
Reference in New Issue