Ctrl K
ComunidadGuías…

internal/gateway/batch_usage_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package gateway

import (
	"context"
	"math"
	"net/http"
	"testing"

	batchstore "github.com/enterpilot/gomodel/internal/batch"
	"github.com/enterpilot/gomodel/internal/core"
	"github.com/enterpilot/gomodel/internal/usage"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

type batchUsageCaptureLogger struct {
	config  usage.Config
	entries []*usage.UsageEntry
}

func (l *batchUsageCaptureLogger) Write(entry *usage.UsageEntry) {
	l.entries = append(l.entries, entry)
}

func (l *batchUsageCaptureLogger) Config() usage.Config { return l.config }
func (l *batchUsageCaptureLogger) Close() error         { return nil }

type staticBatchPricingResolver struct {
	pricing *core.ModelPricing
}

func (r staticBatchPricingResolver) ResolvePricing(_, _ string) *core.ModelPricing {
	return r.pricing
}

func TestLogBatchUsageFromBatchResultsOnlySetsObservedCostComponents(t *testing.T) {
	inputRate := 1.25
	logger := &batchUsageCaptureLogger{config: usage.Config{Enabled: true}}
	stored := &batchstore.StoredBatch{
		Batch: &core.BatchResponse{
			ID:       "batch_cost_components",
			Provider: "openai",
		},
		ProviderName: "openai-byok",
		RequestID:    "req-batch-cost-components",
	}
	result := &core.BatchResultsResponse{
		Object:  "list",
		BatchID: "batch_cost_components",
		Data: []core.BatchResultItem{
			{
				Index:      0,
				StatusCode: 200,
				Model:      "gpt-cost-input-only",
				Provider:   "openai",
				Response: map[string]any{
					"id":    "resp-cost-input-only",
					"model": "gpt-cost-input-only",
					"usage": map[string]any{
						"input_tokens":  float64(1_000_000),
						"output_tokens": float64(10),
						"total_tokens":  float64(1_000_010),
					},
				},
			},
		},
	}

	logged := LogBatchUsageFromBatchResults(
		stored,
		result,
		"",
		logger,
		staticBatchPricingResolver{pricing: &core.ModelPricing{InputPerMtok: &inputRate}},
	)
	require.True(t, logged)
	require.Len(t, logger.entries, 1)
	assert.Equal(t, "openai-byok", logger.entries[0].ProviderName, "rows name the instance, not just its type")
	assert.Equal(t, "openai", logger.entries[0].Provider)

	got := stored.Batch.Usage
	require.NotNil(t, got.InputCost)
	require.Equal(t, inputRate, *got.InputCost)
	require.Nil(t, got.OutputCost)
	require.NotNil(t, got.TotalCost)
	require.Equal(t, inputRate, *got.TotalCost)
}

func TestLogBatchUsageFromBatchResultsUsesXAITicks(t *testing.T) {
	outputRate := 999.0
	logger := &batchUsageCaptureLogger{config: usage.Config{Enabled: true}}
	stored := &batchstore.StoredBatch{
		Batch: &core.BatchResponse{
			ID:       "batch_xai_ticks",
			Provider: "xai",
		},
		RequestID: "req-batch-xai-ticks",
	}
	result := &core.BatchResultsResponse{
		Object:  "list",
		BatchID: "batch_xai_ticks",
		Data: []core.BatchResultItem{
			{
				Index:      0,
				StatusCode: 200,
				Model:      "grok-4.3",
				Provider:   "xai",
				Response: map[string]any{
					"id":    "resp-xai-batch",
					"model": "grok-4.3",
					"usage": map[string]any{
						"input_tokens":      float64(199),
						"output_tokens":     float64(1),
						"total_tokens":      float64(200),
						"cost_in_usd_ticks": float64(158_500),
						"num_sources_used":  float64(2),
					},
				},
			},
		},
	}

	logged := LogBatchUsageFromBatchResults(
		stored,
		result,
		"",
		logger,
		staticBatchPricingResolver{pricing: &core.ModelPricing{OutputPerMtok: &outputRate}},
	)
	require.True(t, logged)
	require.Len(t, logger.entries, 1)

	entry := logger.entries[0]
	require.Equal(t, usage.CostSourceXAITicks, entry.CostSource)
	require.NotNil(t, entry.TotalCost)
	require.LessOrEqual(t, math.Abs(*entry.TotalCost-0.00001585), 1e-12)
	require.Nil(t, entry.InputCost)
	require.Nil(t, entry.OutputCost)
	require.NotNil(t, stored.Batch.Usage.TotalCost)
	require.LessOrEqual(t, math.Abs(*stored.Batch.Usage.TotalCost-0.00001585), 1e-12)
}

// batchResultsProvider answers batch results with one billable item.
type batchResultsProvider struct {
	batchBudgetProvider
}

func (p *batchResultsProvider) GetBatchResults(context.Context, string, string) (*core.BatchResultsResponse, error) {
	return &core.BatchResultsResponse{
		Object: "list",
		Data: []core.BatchResultItem{{
			StatusCode: 200,
			Model:      "gpt-4o-mini",
			Provider:   "openai",
			Response: map[string]any{
				"id":    "resp-1",
				"model": "gpt-4o-mini",
				"usage": map[string]any{"input_tokens": float64(10), "output_tokens": float64(5), "total_tokens": float64(15)},
			},
		}},
	}, nil
}

func TestBatchUsageKeepsSubmittingAuthKey(t *testing.T) {
	logger := &batchUsageCaptureLogger{config: usage.Config{Enabled: true}}
	orchestrator := NewBatchOrchestrator(BatchConfig{
		Provider:    &batchResultsProvider{},
		BatchStore:  batchstore.NewMemoryStore(),
		UsageLogger: logger,
	})

	created, err := orchestrator.Create(core.WithAuthKeyID(context.Background(), "key-submitter"), &core.BatchRequest{
		InputFileID: "file-123",
		Endpoint:    "/v1/chat/completions",
		Metadata:    map[string]string{"provider": "openai"},
	}, BatchMeta{
		RequestID: "req-create",
		Endpoint:  core.DescribeEndpoint(http.MethodPost, "/v1/batches"),
	})
	require.NoError(t, err)

	// Results are read later, possibly with a different key; the usage stays
	// with the key that submitted the batch.
	_, err = orchestrator.Results(core.WithAuthKeyID(context.Background(), "key-reader"), created.Batch.ID, "req-results")
	require.NoError(t, err)
	require.Len(t, logger.entries, 1)
	assert.Equal(t, "key-submitter", logger.entries[0].AuthKeyID)
}