Skip to content

Commit bda7576

Browse files
committed
fix: fall back to embedded pricing in session export
1 parent 4e40823 commit bda7576

3 files changed

Lines changed: 113 additions & 0 deletions

File tree

cmd/agentsview/export.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,9 @@ func runExportSessions(cmd *cobra.Command, cfg exportSessionsConfig) error {
203203
defer database.Close()
204204

205205
ctx := cmd.Context()
206+
if err := ensureExportSessionsPricing(ctx, database, appCfg); err != nil {
207+
return err
208+
}
206209
databaseID, err := database.GetDatabaseID(ctx)
207210
if err != nil {
208211
if errors.Is(err, db.ErrDatabaseIDMissing) {
@@ -244,6 +247,25 @@ func runExportSessions(cmd *cobra.Command, cfg exportSessionsConfig) error {
244247
return enc.Encode(output)
245248
}
246249

250+
// ensureExportSessionsPricing installs embedded fallback plus custom pricing
251+
// for archives whose model_pricing table was never seeded (fresh sync-only
252+
// archives, before serve or usage commands run). The read-only export cannot
253+
// seed the table, and the overlay would override newer fetched rows, so it is
254+
// gated on the table being empty.
255+
func ensureExportSessionsPricing(
256+
ctx context.Context, database *db.DB, appCfg config.Config,
257+
) error {
258+
seeded, err := database.HasModelPricingRows(ctx)
259+
if err != nil {
260+
return fmt.Errorf("checking export pricing: %w", err)
261+
}
262+
if seeded {
263+
return nil
264+
}
265+
applyFallbackPricing(database, appCfg.CustomModelPricing)
266+
return nil
267+
}
268+
247269
func validateExportSessionsCursorFlags(flags *pflag.FlagSet) error {
248270
for _, name := range []string{
249271
"project",

cmd/agentsview/export_sessions_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"go.kenn.io/agentsview/internal/db"
1919
"go.kenn.io/agentsview/internal/dbtest"
2020
"go.kenn.io/agentsview/internal/export"
21+
"go.kenn.io/agentsview/internal/pricing"
2122
)
2223

2324
type exportSessionsDocument struct {
@@ -505,6 +506,81 @@ func firstExportSessionsCursor(t *testing.T) string {
505506
return doc.Cursor.Next
506507
}
507508

509+
func TestExportSessionsFallbackPricingOnUnseededArchive(t *testing.T) {
510+
dataDir := testDataDir(t)
511+
database := dbtest.OpenTestDBAt(t, filepath.Join(dataDir, "sessions.db"))
512+
require.NoError(t, database.SetDatabaseIDForTest(
513+
context.Background(), "fallback-pricing-test-db"))
514+
515+
model := exactFallbackPricedModel(t)
516+
insertExportSessionsTestSession(t, database, db.Session{
517+
ID: "fallback-priced",
518+
Project: "alpha",
519+
Machine: "local",
520+
Agent: "claude",
521+
StartedAt: dbtest.Ptr("2026-06-01T10:00:00Z"),
522+
EndedAt: dbtest.Ptr("2026-06-01T10:10:00Z"),
523+
MessageCount: 3,
524+
UserMessageCount: 2,
525+
})
526+
require.NoError(t, database.InsertMessages([]db.Message{
527+
{
528+
SessionID: "fallback-priced", Ordinal: 0, Role: "user",
529+
Content: "question", ContentLength: len("question"),
530+
Timestamp: "2026-06-01T10:00:00Z",
531+
},
532+
{
533+
SessionID: "fallback-priced", Ordinal: 1, Role: "assistant",
534+
Content: "answer", ContentLength: len("answer"),
535+
Timestamp: "2026-06-01T10:05:00Z", Model: model,
536+
TokenUsage: json.RawMessage(
537+
`{"input_tokens":1000,"output_tokens":500}`),
538+
},
539+
{
540+
SessionID: "fallback-priced", Ordinal: 2, Role: "user",
541+
Content: "follow up", ContentLength: len("follow up"),
542+
Timestamp: "2026-06-01T10:06:00Z",
543+
},
544+
}), "insert messages")
545+
require.NoError(t, database.Close(), "close seeded archive")
546+
547+
stdout, stderr, err := executeExportSessionsCommand(
548+
newRootCommand(), "export", "sessions")
549+
require.NoError(t, err, "export sessions on unseeded archive")
550+
assert.Empty(t, stderr)
551+
552+
doc := decodeExportSessionsDocument(t, stdout)
553+
require.Len(t, doc.Sessions, 1, "exported sessions")
554+
usage := doc.Sessions[0].ModelUsage
555+
require.NotNil(t, usage, "model usage")
556+
assert.True(t, usage.HasCost,
557+
"fallback-priced model %s should have cost", model)
558+
assert.Greater(t, usage.CostUSD, 0.0, "fallback-priced cost")
559+
560+
fallback, ok := doc.Pricing["fallback"].(map[string]any)
561+
require.True(t, ok, "pricing fallback block")
562+
assert.Equal(t, true, fallback["used"], "fallback used")
563+
assert.Contains(t, doc.Pricing["source"], "embedded",
564+
"pricing source provenance")
565+
}
566+
567+
// exactFallbackPricedModel returns an embedded fallback model pattern with
568+
// non-wildcard name and nonzero input/output rates, so lookups resolve
569+
// deterministically regardless of snapshot contents.
570+
func exactFallbackPricedModel(t *testing.T) string {
571+
t.Helper()
572+
for _, p := range pricing.FallbackPricing() {
573+
if strings.ContainsAny(p.ModelPattern, "*/_") {
574+
continue
575+
}
576+
if p.InputPerMTok > 0 && p.OutputPerMTok > 0 {
577+
return p.ModelPattern
578+
}
579+
}
580+
t.Fatal("no exact fallback-priced model in embedded snapshot")
581+
return ""
582+
}
583+
508584
func executeExportSessionsCommand(
509585
root *cobra.Command, args ...string,
510586
) (string, string, error) {

internal/db/pricing.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,21 @@ func (db *DB) InsertMissingModelPricing(
289289

290290
// GetModelPricing returns pricing for an exact model match.
291291
// Returns nil, nil if not found.
292+
// HasModelPricingRows reports whether any non-meta pricing rows are
293+
// stored, using the same meta-row exclusion as pricing map loads.
294+
func (db *DB) HasModelPricingRows(ctx context.Context) (bool, error) {
295+
var exists bool
296+
err := db.getReader().QueryRowContext(ctx,
297+
`SELECT EXISTS(
298+
SELECT 1 FROM model_pricing
299+
WHERE model_pattern NOT LIKE '\_%' ESCAPE '\')`,
300+
).Scan(&exists)
301+
if err != nil {
302+
return false, fmt.Errorf("checking pricing rows: %w", err)
303+
}
304+
return exists, nil
305+
}
306+
292307
func (db *DB) GetModelPricing(
293308
model string,
294309
) (*ModelPricing, error) {

0 commit comments

Comments
 (0)