mirror of
https://gh-proxy.org/https://github.com/klinsmaya/CLIProxyAPI.git
synced 2026-11-08 01:16:47 +08:00
feat(usage): track generate flag with backward-compatible defaults
Propagate client generate metadata through usage records while keeping legacy callers that omit the field enabled by default. Preserve an existing context generate=false when metadata does not set the value, and normalize omission to true only at publishing boundaries.
This commit is contained in:
@@ -1829,6 +1829,7 @@ func (a *usageAdapter) HandleUsage(ctx context.Context, record coreusage.Record)
|
||||
Source: record.Source,
|
||||
ReasoningEffort: record.ReasoningEffort,
|
||||
ServiceTier: record.ServiceTier,
|
||||
Generate: coreusage.GenerateEnabled(record.Generate),
|
||||
RequestedAt: record.RequestedAt,
|
||||
Latency: record.Latency,
|
||||
TTFT: record.TTFT,
|
||||
|
||||
@@ -2138,6 +2138,55 @@ func TestUsageAdapterPanicFusesPlugin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsageAdapterNormalizesOmittedGenerateToTrue(t *testing.T) {
|
||||
var gotGenerate bool
|
||||
plugin := usagePluginFunc(func(ctx context.Context, record pluginapi.UsageRecord) {
|
||||
gotGenerate = record.Generate
|
||||
})
|
||||
host := newHostWithRecords(capabilityRecord{
|
||||
id: "usage-generate",
|
||||
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{
|
||||
UsagePlugin: plugin,
|
||||
}},
|
||||
})
|
||||
adapter := &usageAdapter{
|
||||
host: host,
|
||||
pluginID: "usage-generate",
|
||||
}
|
||||
|
||||
// Legacy callers construct usage.Record without Generate; adapter must publish true.
|
||||
adapter.HandleUsage(context.Background(), coreusage.Record{Provider: "provider", Model: "gpt-5.4"})
|
||||
if !gotGenerate {
|
||||
t.Fatalf("plugin Generate = %v, want true for omitted field", gotGenerate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsageAdapterPreservesExplicitGenerateFalse(t *testing.T) {
|
||||
var gotGenerate bool
|
||||
plugin := usagePluginFunc(func(ctx context.Context, record pluginapi.UsageRecord) {
|
||||
gotGenerate = record.Generate
|
||||
})
|
||||
host := newHostWithRecords(capabilityRecord{
|
||||
id: "usage-generate-false",
|
||||
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{
|
||||
UsagePlugin: plugin,
|
||||
}},
|
||||
})
|
||||
adapter := &usageAdapter{
|
||||
host: host,
|
||||
pluginID: "usage-generate-false",
|
||||
}
|
||||
|
||||
adapter.HandleUsage(context.Background(), coreusage.Record{
|
||||
Provider: "provider",
|
||||
Model: "gpt-5.4",
|
||||
Generate: coreusage.GenerateFlag(false),
|
||||
})
|
||||
if gotGenerate {
|
||||
t.Fatalf("plugin Generate = %v, want false", gotGenerate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsageManagerRegisterNamedReplacesWithoutDuplicateDispatch(t *testing.T) {
|
||||
manager := coreusage.NewManager(0)
|
||||
defer manager.Stop()
|
||||
|
||||
@@ -96,6 +96,7 @@ func (p *usageQueuePlugin) HandleUsage(ctx context.Context, record coreusage.Rec
|
||||
AuthIndex: record.AuthIndex,
|
||||
Tokens: tokens,
|
||||
Failed: failed,
|
||||
Generate: coreusage.GenerateEnabled(record.Generate),
|
||||
Fail: fail,
|
||||
ResponseHeaders: record.ResponseHeaders,
|
||||
}
|
||||
@@ -143,6 +144,7 @@ type requestDetail struct {
|
||||
AuthIndex string `json:"auth_index"`
|
||||
Tokens tokenStats `json:"tokens"`
|
||||
Failed bool `json:"failed"`
|
||||
Generate bool `json:"generate"`
|
||||
Fail failDetail `json:"fail"`
|
||||
ResponseHeaders http.Header `json:"response_headers,omitempty"`
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ func TestUsageQueuePluginPayloadIncludesStableFieldsAndSuccess(t *testing.T) {
|
||||
ReasoningEffort: "medium",
|
||||
ServiceTier: "auto",
|
||||
ResponseServiceTier: "default",
|
||||
Generate: coreusage.GenerateFlag(true),
|
||||
RequestedAt: time.Date(2026, 4, 25, 0, 0, 0, 0, time.UTC),
|
||||
Latency: 1500 * time.Millisecond,
|
||||
Detail: coreusage.Detail{
|
||||
@@ -64,10 +65,51 @@ func TestUsageQueuePluginPayloadIncludesStableFieldsAndSuccess(t *testing.T) {
|
||||
requireHeaderField(t, payload, "response_headers", "X-Upstream-Request-Id", []string{"upstream-req-1"})
|
||||
requireHeaderField(t, payload, "response_headers", "Retry-After", []string{"30"})
|
||||
requireBoolField(t, payload, "failed", false)
|
||||
requireBoolField(t, payload, "generate", true)
|
||||
requireFailField(t, payload, http.StatusOK, "")
|
||||
})
|
||||
}
|
||||
|
||||
func TestUsageQueuePluginPayloadIncludesGenerateFalse(t *testing.T) {
|
||||
withEnabledQueue(t, func() {
|
||||
ctx := internallogging.WithResponseStatusHolder(context.Background())
|
||||
internallogging.SetResponseStatus(ctx, http.StatusOK)
|
||||
|
||||
(&usageQueuePlugin{}).HandleUsage(ctx, coreusage.Record{
|
||||
Provider: "openai",
|
||||
Model: "gpt-5.4",
|
||||
Generate: coreusage.GenerateFlag(false),
|
||||
Detail: coreusage.Detail{
|
||||
InputTokens: 1,
|
||||
TotalTokens: 1,
|
||||
},
|
||||
})
|
||||
|
||||
payload := popSinglePayload(t)
|
||||
requireBoolField(t, payload, "generate", false)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUsageQueuePluginPayloadDefaultsGenerateTrueWhenOmitted(t *testing.T) {
|
||||
withEnabledQueue(t, func() {
|
||||
ctx := internallogging.WithResponseStatusHolder(context.Background())
|
||||
internallogging.SetResponseStatus(ctx, http.StatusOK)
|
||||
|
||||
// Legacy callers construct usage.Record without Generate; omission must publish as true.
|
||||
(&usageQueuePlugin{}).HandleUsage(ctx, coreusage.Record{
|
||||
Provider: "openai",
|
||||
Model: "gpt-5.4",
|
||||
Detail: coreusage.Detail{
|
||||
InputTokens: 1,
|
||||
TotalTokens: 1,
|
||||
},
|
||||
})
|
||||
|
||||
payload := popSinglePayload(t)
|
||||
requireBoolField(t, payload, "generate", true)
|
||||
})
|
||||
}
|
||||
|
||||
func TestUsageQueuePluginMarksCanonicalZeroCacheRead(t *testing.T) {
|
||||
withEnabledQueue(t, func() {
|
||||
ctx := internallogging.WithResponseStatusHolder(context.Background())
|
||||
|
||||
@@ -33,6 +33,7 @@ type UsageReporter struct {
|
||||
source string
|
||||
reasoning string
|
||||
serviceTier string
|
||||
generate bool
|
||||
requestedAt time.Time
|
||||
ttftMu sync.RWMutex
|
||||
ttft time.Duration
|
||||
@@ -71,6 +72,7 @@ func NewUsageReporter(ctx context.Context, provider, model string, auth *cliprox
|
||||
authType: resolveUsageAuthType(auth),
|
||||
reasoning: usage.ReasoningEffortFromContext(ctx),
|
||||
serviceTier: usage.ServiceTierFromContext(ctx),
|
||||
generate: usage.GenerateFromContext(ctx),
|
||||
}
|
||||
if auth != nil {
|
||||
reporter.authID = auth.ID
|
||||
@@ -249,14 +251,14 @@ func (r *UsageReporter) buildRecord(detail usage.Detail, failed bool, failures .
|
||||
fail = failures[0]
|
||||
}
|
||||
if r == nil {
|
||||
return usage.Record{Detail: detail, Failed: failed, Fail: fail}
|
||||
return usage.Record{Detail: detail, Failed: failed, Fail: fail, Generate: usage.GenerateFlag(true)}
|
||||
}
|
||||
return r.buildRecordForModel(r.model, detail, failed, fail)
|
||||
}
|
||||
|
||||
func (r *UsageReporter) buildRecordForModel(model string, detail usage.Detail, failed bool, fail usage.Failure) usage.Record {
|
||||
if r == nil {
|
||||
return usage.Record{Model: model, Detail: detail, Failed: failed, Fail: fail}
|
||||
return usage.Record{Model: model, Detail: detail, Failed: failed, Fail: fail, Generate: usage.GenerateFlag(true)}
|
||||
}
|
||||
return usage.Record{
|
||||
Provider: r.provider,
|
||||
@@ -271,6 +273,7 @@ func (r *UsageReporter) buildRecordForModel(model string, detail usage.Detail, f
|
||||
ReasoningEffort: r.reasoning,
|
||||
ServiceTier: r.serviceTier,
|
||||
ResponseServiceTier: strings.TrimSpace(detail.ResponseServiceTier),
|
||||
Generate: usage.GenerateFlag(r.generate),
|
||||
RequestedAt: r.requestedAt,
|
||||
Latency: r.latency(),
|
||||
TTFT: r.ttftDuration(),
|
||||
|
||||
@@ -469,6 +469,25 @@ func TestUsageReporterBuildRecordIncludesServiceTier(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsageReporterBuildRecordDefaultsGenerateTrue(t *testing.T) {
|
||||
reporter := NewUsageReporter(context.Background(), "openai", "gpt-5.4", nil)
|
||||
|
||||
record := reporter.buildRecord(usage.Detail{TotalTokens: 3}, false)
|
||||
if !usage.GenerateEnabled(record.Generate) {
|
||||
t.Fatalf("generate = %v, want true", usage.GenerateEnabled(record.Generate))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsageReporterBuildRecordIncludesGenerateFalse(t *testing.T) {
|
||||
ctx := usage.WithGenerate(context.Background(), false)
|
||||
reporter := NewUsageReporter(ctx, "openai", "gpt-5.4", nil)
|
||||
|
||||
record := reporter.buildRecord(usage.Detail{TotalTokens: 3}, false)
|
||||
if usage.GenerateEnabled(record.Generate) {
|
||||
t.Fatalf("generate = %v, want false", usage.GenerateEnabled(record.Generate))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsageReporterSetTranslatedReasoningEffortPreservesClientServiceTier(t *testing.T) {
|
||||
ctx := usage.WithServiceTier(context.Background(), "auto")
|
||||
reporter := NewUsageReporter(ctx, "openai", "gpt-5.4", nil)
|
||||
|
||||
@@ -329,6 +329,19 @@ func setServiceTierMetadata(meta map[string]any, rawJSON []byte) {
|
||||
meta[coreexecutor.ServiceTierMetadataKey] = serviceTier
|
||||
}
|
||||
|
||||
func setGenerateMetadata(meta map[string]any, rawJSON []byte) {
|
||||
if meta == nil {
|
||||
return
|
||||
}
|
||||
// Missing or true means generation is enabled; only an explicit false disables generation.
|
||||
generate := true
|
||||
node := gjson.GetBytes(rawJSON, "generate")
|
||||
if node.Exists() && node.IsBool() && !node.Bool() {
|
||||
generate = false
|
||||
}
|
||||
meta[coreexecutor.GenerateMetadataKey] = generate
|
||||
}
|
||||
|
||||
// headersFromContext extracts the original HTTP request headers from the gin context
|
||||
// embedded in the provided context. This allows session affinity selectors to read
|
||||
// client-provided session headers.
|
||||
@@ -739,6 +752,7 @@ func (h *BaseAPIHandler) executeWithAuthManagerFormats(ctx context.Context, entr
|
||||
addModelExecutionSourceMetadata(reqMeta, execOptions.InternalSource)
|
||||
setReasoningEffortMetadata(reqMeta, entryProtocol, normalizedModel, rawJSON)
|
||||
setServiceTierMetadata(reqMeta, rawJSON)
|
||||
setGenerateMetadata(reqMeta, rawJSON)
|
||||
payload := rawJSON
|
||||
if len(payload) == 0 {
|
||||
payload = nil
|
||||
@@ -806,6 +820,7 @@ func (h *BaseAPIHandler) executeCountWithAuthManager(ctx context.Context, handle
|
||||
addAuthSelectionModelMetadata(reqMeta, execOptions.AuthSelectionModel)
|
||||
setReasoningEffortMetadata(reqMeta, handlerType, normalizedModel, rawJSON)
|
||||
setServiceTierMetadata(reqMeta, rawJSON)
|
||||
setGenerateMetadata(reqMeta, rawJSON)
|
||||
payload := rawJSON
|
||||
if len(payload) == 0 {
|
||||
payload = nil
|
||||
@@ -893,6 +908,7 @@ func (h *BaseAPIHandler) pluginExecutorRequest(ctx context.Context, entryProtoco
|
||||
addModelExecutionSourceMetadata(reqMeta, execOptions.InternalSource)
|
||||
setReasoningEffortMetadata(reqMeta, entryProtocol, modelName, rawJSON)
|
||||
setServiceTierMetadata(reqMeta, rawJSON)
|
||||
setGenerateMetadata(reqMeta, rawJSON)
|
||||
payload := rawJSON
|
||||
if len(payload) == 0 {
|
||||
payload = nil
|
||||
@@ -1140,6 +1156,7 @@ func (h *BaseAPIHandler) executeStreamWithAuthManagerFormats(ctx context.Context
|
||||
addModelExecutionSourceMetadata(reqMeta, execOptions.InternalSource)
|
||||
setReasoningEffortMetadata(reqMeta, entryProtocol, normalizedModel, rawJSON)
|
||||
setServiceTierMetadata(reqMeta, rawJSON)
|
||||
setGenerateMetadata(reqMeta, rawJSON)
|
||||
payload := rawJSON
|
||||
if len(payload) == 0 {
|
||||
payload = nil
|
||||
|
||||
@@ -70,3 +70,33 @@ func TestSetServiceTierMetadataPreservesExplicitDefault(t *testing.T) {
|
||||
t.Fatalf("ServiceTierMetadataKey = %v, want %q", gotServiceTier, "default")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetGenerateMetadataDefaultsWhenMissing(t *testing.T) {
|
||||
meta := make(map[string]any)
|
||||
|
||||
setGenerateMetadata(meta, []byte(`{"model":"gpt-5.4"}`))
|
||||
|
||||
if got := meta[coreexecutor.GenerateMetadataKey]; got != true {
|
||||
t.Fatalf("GenerateMetadataKey = %v, want true", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetGenerateMetadataPreservesTrue(t *testing.T) {
|
||||
meta := make(map[string]any)
|
||||
|
||||
setGenerateMetadata(meta, []byte(`{"generate":true}`))
|
||||
|
||||
if got := meta[coreexecutor.GenerateMetadataKey]; got != true {
|
||||
t.Fatalf("GenerateMetadataKey = %v, want true", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetGenerateMetadataHonorsExplicitFalse(t *testing.T) {
|
||||
meta := make(map[string]any)
|
||||
|
||||
setGenerateMetadata(meta, []byte(`{"generate":false}`))
|
||||
|
||||
if got := meta[coreexecutor.GenerateMetadataKey]; got != false {
|
||||
t.Fatalf("GenerateMetadataKey = %v, want false", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3050,6 +3050,9 @@ func contextWithRequestedModelAlias(ctx context.Context, opts cliproxyexecutor.O
|
||||
if serviceTier != "" {
|
||||
ctx = coreusage.WithServiceTier(ctx, serviceTier)
|
||||
}
|
||||
if generate, ok := generateFromOptions(opts); ok {
|
||||
ctx = coreusage.WithGenerate(ctx, generate)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
@@ -3100,6 +3103,22 @@ func serviceTierFromOptions(opts cliproxyexecutor.Options) string {
|
||||
return stringMetadataValue(opts.Metadata, cliproxyexecutor.ServiceTierMetadataKey)
|
||||
}
|
||||
|
||||
func generateFromOptions(opts cliproxyexecutor.Options) (bool, bool) {
|
||||
if len(opts.Metadata) == 0 {
|
||||
return false, false
|
||||
}
|
||||
raw, ok := opts.Metadata[cliproxyexecutor.GenerateMetadataKey]
|
||||
if !ok || raw == nil {
|
||||
return false, false
|
||||
}
|
||||
switch value := raw.(type) {
|
||||
case bool:
|
||||
return value, true
|
||||
default:
|
||||
return false, false
|
||||
}
|
||||
}
|
||||
|
||||
func stringMetadataValue(metadata map[string]any, key string) string {
|
||||
if len(metadata) == 0 {
|
||||
return ""
|
||||
|
||||
@@ -14,6 +14,7 @@ func TestContextWithRequestedModelAliasIncludesReasoningEffort(t *testing.T) {
|
||||
cliproxyexecutor.RequestedModelMetadataKey: "client-model",
|
||||
cliproxyexecutor.ReasoningEffortMetadataKey: "medium",
|
||||
cliproxyexecutor.ServiceTierMetadataKey: "auto",
|
||||
cliproxyexecutor.GenerateMetadataKey: false,
|
||||
},
|
||||
}, "fallback-model")
|
||||
|
||||
@@ -27,4 +28,32 @@ func TestContextWithRequestedModelAliasIncludesReasoningEffort(t *testing.T) {
|
||||
if gotServiceTier != "auto" {
|
||||
t.Fatalf("service tier = %q, want %q", gotServiceTier, "auto")
|
||||
}
|
||||
if got := coreusage.GenerateFromContext(ctx); got {
|
||||
t.Fatalf("generate = %v, want false", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextWithRequestedModelAliasDefaultsGenerateTrue(t *testing.T) {
|
||||
ctx := contextWithRequestedModelAlias(context.Background(), cliproxyexecutor.Options{
|
||||
Metadata: map[string]any{
|
||||
cliproxyexecutor.RequestedModelMetadataKey: "client-model",
|
||||
},
|
||||
}, "fallback-model")
|
||||
|
||||
if got := coreusage.GenerateFromContext(ctx); !got {
|
||||
t.Fatalf("generate = %v, want true", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextWithRequestedModelAliasPreservesExistingGenerateFalse(t *testing.T) {
|
||||
ctx := coreusage.WithGenerate(context.Background(), false)
|
||||
ctx = contextWithRequestedModelAlias(ctx, cliproxyexecutor.Options{
|
||||
Metadata: map[string]any{
|
||||
cliproxyexecutor.RequestedModelMetadataKey: "client-model",
|
||||
},
|
||||
}, "fallback-model")
|
||||
|
||||
if got := coreusage.GenerateFromContext(ctx); got {
|
||||
t.Fatalf("generate = %v, want false", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ const ReasoningEffortMetadataKey = "reasoning_effort"
|
||||
// ServiceTierMetadataKey stores the client-requested service tier for usage logs.
|
||||
const ServiceTierMetadataKey = "service_tier"
|
||||
|
||||
// GenerateMetadataKey stores whether the client requested actual generation for usage logs.
|
||||
// Missing or true means generation is enabled; only an explicit false disables generation.
|
||||
const GenerateMetadataKey = "generate"
|
||||
|
||||
const (
|
||||
// PinnedAuthMetadataKey locks execution to a specific auth ID.
|
||||
PinnedAuthMetadataKey = "pinned_auth_id"
|
||||
|
||||
@@ -39,12 +39,16 @@ type Record struct {
|
||||
RequestServiceTier string
|
||||
// ResponseServiceTier stores the final tier reported by the upstream response.
|
||||
ResponseServiceTier string
|
||||
RequestedAt time.Time
|
||||
Latency time.Duration
|
||||
TTFT time.Duration
|
||||
Failed bool
|
||||
Fail Failure
|
||||
Detail Detail
|
||||
// Generate reports whether the client requested actual generation.
|
||||
// nil or true means generation is enabled; only an explicit false disables generation.
|
||||
// Use GenerateFlag to set the value and GenerateEnabled to read it with the default.
|
||||
Generate *bool
|
||||
RequestedAt time.Time
|
||||
Latency time.Duration
|
||||
TTFT time.Duration
|
||||
Failed bool
|
||||
Fail Failure
|
||||
Detail Detail
|
||||
// ResponseHeaders stores a snapshot of upstream response headers for usage sinks.
|
||||
ResponseHeaders http.Header
|
||||
}
|
||||
@@ -70,6 +74,7 @@ type Detail struct {
|
||||
type requestedModelAliasContextKey struct{}
|
||||
type reasoningEffortContextKey struct{}
|
||||
type serviceTierContextKey struct{}
|
||||
type generateContextKey struct{}
|
||||
|
||||
// WithRequestedModelAlias stores the client-requested model name for usage sinks.
|
||||
func WithRequestedModelAlias(ctx context.Context, alias string) context.Context {
|
||||
@@ -163,6 +168,44 @@ func ServiceTierFromContext(ctx context.Context) string {
|
||||
}
|
||||
}
|
||||
|
||||
// WithGenerate stores whether the client requested actual generation for usage sinks.
|
||||
// Missing context values default to true; only an explicit false disables generation.
|
||||
func WithGenerate(ctx context.Context, generate bool) context.Context {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
return context.WithValue(ctx, generateContextKey{}, generate)
|
||||
}
|
||||
|
||||
// GenerateFromContext returns whether the client requested actual generation.
|
||||
// Missing values default to true.
|
||||
func GenerateFromContext(ctx context.Context) bool {
|
||||
if ctx == nil {
|
||||
return true
|
||||
}
|
||||
raw := ctx.Value(generateContextKey{})
|
||||
switch value := raw.(type) {
|
||||
case bool:
|
||||
return value
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateFlag returns a pointer suitable for Record.Generate.
|
||||
func GenerateFlag(generate bool) *bool {
|
||||
return &generate
|
||||
}
|
||||
|
||||
// GenerateEnabled reports whether generation is enabled for the record field.
|
||||
// A nil value defaults to true so legacy callers that omit Generate keep the historical behavior.
|
||||
func GenerateEnabled(generate *bool) bool {
|
||||
if generate == nil {
|
||||
return true
|
||||
}
|
||||
return *generate
|
||||
}
|
||||
|
||||
// Plugin consumes usage records emitted by the proxy runtime.
|
||||
type Plugin interface {
|
||||
HandleUsage(ctx context.Context, record Record)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package usage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGenerateEnabledDefaultsNilToTrue(t *testing.T) {
|
||||
if !GenerateEnabled(nil) {
|
||||
t.Fatalf("GenerateEnabled(nil) = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateEnabledHonorsExplicitFalse(t *testing.T) {
|
||||
if GenerateEnabled(GenerateFlag(false)) {
|
||||
t.Fatalf("GenerateEnabled(false) = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateEnabledHonorsExplicitTrue(t *testing.T) {
|
||||
if !GenerateEnabled(GenerateFlag(true)) {
|
||||
t.Fatalf("GenerateEnabled(true) = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateFromContextDefaultsMissingToTrue(t *testing.T) {
|
||||
if !GenerateFromContext(context.Background()) {
|
||||
t.Fatalf("GenerateFromContext(background) = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateFromContextHonorsExplicitFalse(t *testing.T) {
|
||||
ctx := WithGenerate(context.Background(), false)
|
||||
if GenerateFromContext(ctx) {
|
||||
t.Fatalf("GenerateFromContext(false) = true, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordOmittedGenerateIsEnabled(t *testing.T) {
|
||||
// Existing callers construct Record without setting Generate.
|
||||
// Omission must remain distinguishable from explicit false and default to true.
|
||||
record := Record{
|
||||
Provider: "openai",
|
||||
Model: "gpt-5.4",
|
||||
}
|
||||
if record.Generate != nil {
|
||||
t.Fatalf("Record.Generate = %v, want nil for omitted field", record.Generate)
|
||||
}
|
||||
if !GenerateEnabled(record.Generate) {
|
||||
t.Fatalf("GenerateEnabled(omitted) = false, want true")
|
||||
}
|
||||
}
|
||||
@@ -1276,6 +1276,9 @@ type UsageRecord struct {
|
||||
ReasoningEffort string
|
||||
// ServiceTier records the requested or reported service tier.
|
||||
ServiceTier string
|
||||
// Generate reports whether the client requested actual generation.
|
||||
// The host normalizes omitted usage.Record values to true before delivery.
|
||||
Generate bool
|
||||
// RequestedAt is the time the request was received.
|
||||
RequestedAt time.Time
|
||||
// Latency is the total request latency.
|
||||
|
||||
Reference in New Issue
Block a user