Add reasoning effort to usage events
This commit is contained in:
@@ -231,6 +231,17 @@ func requestExecutionMetadata(ctx context.Context) map[string]any {
|
||||
return meta
|
||||
}
|
||||
|
||||
func setReasoningEffortMetadata(meta map[string]any, handlerType, model string, rawJSON []byte) {
|
||||
if meta == nil {
|
||||
return
|
||||
}
|
||||
effort := thinking.ExtractReasoningEffort(rawJSON, handlerType, model)
|
||||
if effort == "" {
|
||||
return
|
||||
}
|
||||
meta[coreexecutor.ReasoningEffortMetadataKey] = effort
|
||||
}
|
||||
|
||||
// headersFromContext extracts the original HTTP request headers from the gin context
|
||||
// embedded in the provided context. This allows session affinity selectors to read
|
||||
// client headers like X-Amp-Thread-Id.
|
||||
@@ -550,6 +561,7 @@ func (h *BaseAPIHandler) executeWithAuthManager(ctx context.Context, handlerType
|
||||
}
|
||||
reqMeta := requestExecutionMetadata(ctx)
|
||||
reqMeta[coreexecutor.RequestedModelMetadataKey] = modelName
|
||||
setReasoningEffortMetadata(reqMeta, handlerType, normalizedModel, rawJSON)
|
||||
payload := rawJSON
|
||||
if len(payload) == 0 {
|
||||
payload = nil
|
||||
@@ -598,6 +610,7 @@ func (h *BaseAPIHandler) ExecuteCountWithAuthManager(ctx context.Context, handle
|
||||
}
|
||||
reqMeta := requestExecutionMetadata(ctx)
|
||||
reqMeta[coreexecutor.RequestedModelMetadataKey] = modelName
|
||||
setReasoningEffortMetadata(reqMeta, handlerType, normalizedModel, rawJSON)
|
||||
payload := rawJSON
|
||||
if len(payload) == 0 {
|
||||
payload = nil
|
||||
@@ -659,6 +672,7 @@ func (h *BaseAPIHandler) executeStreamWithAuthManager(ctx context.Context, handl
|
||||
}
|
||||
reqMeta := requestExecutionMetadata(ctx)
|
||||
reqMeta[coreexecutor.RequestedModelMetadataKey] = modelName
|
||||
setReasoningEffortMetadata(reqMeta, handlerType, normalizedModel, rawJSON)
|
||||
payload := rawJSON
|
||||
if len(payload) == 0 {
|
||||
payload = nil
|
||||
|
||||
@@ -18,3 +18,23 @@ func TestRequestExecutionMetadataIncludesExecutionSessionWithoutIdempotencyKey(t
|
||||
t.Fatalf("unexpected idempotency key in metadata: %v", meta[idempotencyKeyMetadataKey])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetReasoningEffortMetadataUsesSuffixOverBody(t *testing.T) {
|
||||
meta := make(map[string]any)
|
||||
|
||||
setReasoningEffortMetadata(meta, "openai", "gpt-5.4(high)", []byte(`{"reasoning_effort":"low"}`))
|
||||
|
||||
if got := meta[coreexecutor.ReasoningEffortMetadataKey]; got != "high" {
|
||||
t.Fatalf("ReasoningEffortMetadataKey = %v, want %q", got, "high")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetReasoningEffortMetadataSupportsOpenAIResponses(t *testing.T) {
|
||||
meta := make(map[string]any)
|
||||
|
||||
setReasoningEffortMetadata(meta, "openai-response", "gpt-5.4", []byte(`{"reasoning":{"effort":"medium"}}`))
|
||||
|
||||
if got := meta[coreexecutor.ReasoningEffortMetadataKey]; got != "medium" {
|
||||
t.Fatalf("ReasoningEffortMetadataKey = %v, want %q", got, "medium")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1632,7 +1632,11 @@ func hasRequestedModelMetadata(meta map[string]any) bool {
|
||||
|
||||
func contextWithRequestedModelAlias(ctx context.Context, opts cliproxyexecutor.Options, fallback string) context.Context {
|
||||
alias := requestedModelAliasFromOptions(opts, fallback)
|
||||
return coreusage.WithRequestedModelAlias(ctx, alias)
|
||||
ctx = coreusage.WithRequestedModelAlias(ctx, alias)
|
||||
if effort := reasoningEffortFromOptions(opts); effort != "" {
|
||||
ctx = coreusage.WithReasoningEffort(ctx, effort)
|
||||
}
|
||||
return ctx
|
||||
}
|
||||
|
||||
func requestedModelAliasFromOptions(opts cliproxyexecutor.Options, fallback string) string {
|
||||
@@ -1660,6 +1664,24 @@ func requestedModelAliasFromOptions(opts cliproxyexecutor.Options, fallback stri
|
||||
}
|
||||
}
|
||||
|
||||
func reasoningEffortFromOptions(opts cliproxyexecutor.Options) string {
|
||||
if len(opts.Metadata) == 0 {
|
||||
return ""
|
||||
}
|
||||
raw, ok := opts.Metadata[cliproxyexecutor.ReasoningEffortMetadataKey]
|
||||
if !ok || raw == nil {
|
||||
return ""
|
||||
}
|
||||
switch value := raw.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(value)
|
||||
case []byte:
|
||||
return strings.TrimSpace(string(value))
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func pinnedAuthIDFromMetadata(meta map[string]any) string {
|
||||
if len(meta) == 0 {
|
||||
return ""
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
|
||||
coreusage "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage"
|
||||
)
|
||||
|
||||
func TestContextWithRequestedModelAliasIncludesReasoningEffort(t *testing.T) {
|
||||
ctx := contextWithRequestedModelAlias(context.Background(), cliproxyexecutor.Options{
|
||||
Metadata: map[string]any{
|
||||
cliproxyexecutor.RequestedModelMetadataKey: "client-model",
|
||||
cliproxyexecutor.ReasoningEffortMetadataKey: "medium",
|
||||
},
|
||||
}, "fallback-model")
|
||||
|
||||
if got := coreusage.RequestedModelAliasFromContext(ctx); got != "client-model" {
|
||||
t.Fatalf("requested model alias = %q, want %q", got, "client-model")
|
||||
}
|
||||
if got := coreusage.ReasoningEffortFromContext(ctx); got != "medium" {
|
||||
t.Fatalf("reasoning effort = %q, want %q", got, "medium")
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,9 @@ const RequestPathMetadataKey = "request_path"
|
||||
// DisallowFreeAuthMetadataKey instructs auth selection to skip known free-tier credentials.
|
||||
const DisallowFreeAuthMetadataKey = "disallow_free_auth"
|
||||
|
||||
// ReasoningEffortMetadataKey stores the client-requested reasoning effort for usage logs.
|
||||
const ReasoningEffortMetadataKey = "reasoning_effort"
|
||||
|
||||
const (
|
||||
// PinnedAuthMetadataKey locks execution to a specific auth ID.
|
||||
PinnedAuthMetadataKey = "pinned_auth_id"
|
||||
|
||||
@@ -12,19 +12,21 @@ import (
|
||||
|
||||
// Record contains the usage statistics captured for a single provider request.
|
||||
type Record struct {
|
||||
Provider string
|
||||
Model string
|
||||
Alias string
|
||||
APIKey string
|
||||
AuthID string
|
||||
AuthIndex string
|
||||
AuthType string
|
||||
Source string
|
||||
RequestedAt time.Time
|
||||
Latency time.Duration
|
||||
Failed bool
|
||||
Fail Failure
|
||||
Detail Detail
|
||||
Provider string
|
||||
Model string
|
||||
Alias string
|
||||
APIKey string
|
||||
AuthID string
|
||||
AuthIndex string
|
||||
AuthType string
|
||||
Source string
|
||||
// ReasoningEffort stores the client-requested thinking level for request event logs.
|
||||
ReasoningEffort string
|
||||
RequestedAt time.Time
|
||||
Latency time.Duration
|
||||
Failed bool
|
||||
Fail Failure
|
||||
Detail Detail
|
||||
// ResponseHeaders stores a snapshot of upstream response headers for usage sinks.
|
||||
ResponseHeaders http.Header
|
||||
}
|
||||
@@ -47,6 +49,7 @@ type Detail struct {
|
||||
}
|
||||
|
||||
type requestedModelAliasContextKey struct{}
|
||||
type reasoningEffortContextKey struct{}
|
||||
|
||||
// WithRequestedModelAlias stores the client-requested model name for usage sinks.
|
||||
func WithRequestedModelAlias(ctx context.Context, alias string) context.Context {
|
||||
@@ -76,6 +79,34 @@ func RequestedModelAliasFromContext(ctx context.Context) string {
|
||||
}
|
||||
}
|
||||
|
||||
// WithReasoningEffort stores the client-requested reasoning effort for usage sinks.
|
||||
func WithReasoningEffort(ctx context.Context, effort string) context.Context {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
effort = strings.TrimSpace(effort)
|
||||
if effort == "" {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, reasoningEffortContextKey{}, effort)
|
||||
}
|
||||
|
||||
// ReasoningEffortFromContext returns the client-requested reasoning effort stored in ctx.
|
||||
func ReasoningEffortFromContext(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
raw := ctx.Value(reasoningEffortContextKey{})
|
||||
switch value := raw.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(value)
|
||||
case []byte:
|
||||
return strings.TrimSpace(string(value))
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Plugin consumes usage records emitted by the proxy runtime.
|
||||
type Plugin interface {
|
||||
HandleUsage(ctx context.Context, record Record)
|
||||
|
||||
Reference in New Issue
Block a user