fs: show OpenRouter cache pricing for selected models
This commit is contained in:
parent
3ca84487c5
commit
2e30b0ddab
|
|
@ -29,8 +29,10 @@ type openAIModelInfo struct {
|
||||||
ContextLength int `json:"context_length"`
|
ContextLength int `json:"context_length"`
|
||||||
// OpenRouter pricing fields (per-token, as string)
|
// OpenRouter pricing fields (per-token, as string)
|
||||||
Pricing *struct {
|
Pricing *struct {
|
||||||
Prompt string `json:"prompt"`
|
Prompt string `json:"prompt"`
|
||||||
Completion string `json:"completion"`
|
Completion string `json:"completion"`
|
||||||
|
InputCacheRead string `json:"input_cache_read"`
|
||||||
|
InputCacheWrite string `json:"input_cache_write"`
|
||||||
} `json:"pricing,omitempty"`
|
} `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 {
|
if v, err := strconv.ParseFloat(m.Pricing.Completion, 64); err == nil {
|
||||||
pricing.Output = v
|
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
|
info[i].Pricing = &pricing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -505,8 +505,10 @@ func TestModelsInfo_ParsesPricing(t *testing.T) {
|
||||||
"id": "test-model",
|
"id": "test-model",
|
||||||
"context_length": 128000,
|
"context_length": 128000,
|
||||||
"pricing": map[string]string{
|
"pricing": map[string]string{
|
||||||
"prompt": "0.000003",
|
"prompt": "0.000003",
|
||||||
"completion": "0.000015",
|
"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 {
|
if info[0].Pricing.Output != 0.000015 {
|
||||||
t.Errorf("Output = %v, want 0.000015", info[0].Pricing.Output)
|
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
|
// Second model has no pricing
|
||||||
if info[1].ID != "no-pricing-model" {
|
if info[1].ID != "no-pricing-model" {
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,39 @@ func writeModelLine(sb *strings.Builder, backendName, modelID string, pricing *b
|
||||||
sb.WriteByte('\n')
|
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.
|
// 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.
|
// Returns "-" if the value is zero. Appends "(e)" if estimated is true and value is non-zero.
|
||||||
func formatPricingWithEstimate(perToken float64, estimated bool) string {
|
func formatPricingWithEstimate(perToken float64, estimated bool) string {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,90 @@
|
||||||
package fs
|
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) {
|
func TestFormatPricingWithEstimate(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
|
|
||||||
|
|
@ -1000,10 +1000,12 @@ func buildAgentChildren(a *agent.Agent, s *session.Session) []virtfs.FsNodeDecl
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
name := strings.Join(args, " ")
|
||||||
if be := a.Backend(); be != nil {
|
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) {
|
"models": func(_ []string) ([]byte, error) {
|
||||||
return []byte(s.CachedListModels() + "\n"), nil
|
return []byte(s.CachedListModels() + "\n"), nil
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue