Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
316 lines
7.8 KiB
Go
316 lines
7.8 KiB
Go
package model
|
|
|
|
import (
|
|
"encoding/json"
|
|
"maps"
|
|
"slices"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
)
|
|
|
|
const (
|
|
logOtherAdminInfoKey = "admin_info"
|
|
logOtherRootInfoKey = "root_info"
|
|
logOtherAuditInfoKey = "audit_info"
|
|
)
|
|
|
|
// legacySensitiveLogOtherKeys are historical top-level fields that must never
|
|
// be written via SetPublic and must be stripped from user-visible projections.
|
|
var legacySensitiveLogOtherKeys = []string{
|
|
"channel_id",
|
|
"channel_name",
|
|
"channel_type",
|
|
"reject_reason",
|
|
}
|
|
|
|
type logOtherVisibility int
|
|
|
|
const (
|
|
logOtherVisibilityUser logOtherVisibility = iota
|
|
logOtherVisibilityAdmin
|
|
logOtherVisibilityRoot
|
|
)
|
|
|
|
// LogOther separates usage-log metadata by the audience allowed to see it.
|
|
// Its maps stay private so callers cannot accidentally place privileged fields
|
|
// in the user-visible top level.
|
|
type LogOther struct {
|
|
public map[string]any
|
|
adminInfo map[string]any
|
|
rootInfo map[string]any
|
|
auditInfo map[string]any
|
|
}
|
|
|
|
func NewLogOther() *LogOther {
|
|
return &LogOther{}
|
|
}
|
|
|
|
func isReservedLogOtherKey(key string) bool {
|
|
switch key {
|
|
case logOtherAdminInfoKey, logOtherRootInfoKey, logOtherAuditInfoKey:
|
|
return true
|
|
default:
|
|
return slices.Contains(legacySensitiveLogOtherKeys, key)
|
|
}
|
|
}
|
|
|
|
// SetPublic records metadata that log owners may receive from self/token log APIs.
|
|
// It rejects role-scoped and legacy-sensitive keys so new writers cannot recreate
|
|
// the historical channel/reject-reason leak.
|
|
func (o *LogOther) SetPublic(key string, value any) bool {
|
|
if o == nil || key == "" || isReservedLogOtherKey(key) {
|
|
return false
|
|
}
|
|
if o.public == nil {
|
|
o.public = make(map[string]any)
|
|
}
|
|
o.public[key] = value
|
|
return true
|
|
}
|
|
|
|
func (o *LogOther) MergePublic(values map[string]any) {
|
|
for key, value := range values {
|
|
o.SetPublic(key, value)
|
|
}
|
|
}
|
|
|
|
func (o *LogOther) SetAdmin(key string, value any) bool {
|
|
if o == nil || key == "" {
|
|
return false
|
|
}
|
|
if o.adminInfo == nil {
|
|
o.adminInfo = make(map[string]any)
|
|
}
|
|
o.adminInfo[key] = value
|
|
return true
|
|
}
|
|
|
|
func (o *LogOther) MergeAdmin(values map[string]any) {
|
|
for key, value := range values {
|
|
o.SetAdmin(key, value)
|
|
}
|
|
}
|
|
|
|
func (o *LogOther) SetRoot(key string, value any) bool {
|
|
if o == nil || key == "" {
|
|
return false
|
|
}
|
|
if o.rootInfo == nil {
|
|
o.rootInfo = make(map[string]any)
|
|
}
|
|
o.rootInfo[key] = value
|
|
return true
|
|
}
|
|
|
|
func (o *LogOther) MergeRoot(values map[string]any) {
|
|
for key, value := range values {
|
|
o.SetRoot(key, value)
|
|
}
|
|
}
|
|
|
|
func (o *LogOther) SetAudit(key string, value any) bool {
|
|
if o == nil || key == "" {
|
|
return false
|
|
}
|
|
if o.auditInfo == nil {
|
|
o.auditInfo = make(map[string]any)
|
|
}
|
|
o.auditInfo[key] = value
|
|
return true
|
|
}
|
|
|
|
func (o *LogOther) MergeAudit(values map[string]any) {
|
|
for key, value := range values {
|
|
o.SetAudit(key, value)
|
|
}
|
|
}
|
|
|
|
func copyLogOtherMap(values map[string]any) map[string]any {
|
|
if len(values) == 0 {
|
|
return nil
|
|
}
|
|
copyValues := make(map[string]any, len(values))
|
|
maps.Copy(copyValues, values)
|
|
return copyValues
|
|
}
|
|
|
|
func (o *LogOther) toMap() map[string]any {
|
|
result := make(map[string]any)
|
|
if o == nil {
|
|
return result
|
|
}
|
|
|
|
maps.Copy(result, o.public)
|
|
if adminInfo := copyLogOtherMap(o.adminInfo); len(adminInfo) > 0 {
|
|
result[logOtherAdminInfoKey] = adminInfo
|
|
}
|
|
if auditInfo := copyLogOtherMap(o.auditInfo); len(auditInfo) > 0 {
|
|
result[logOtherAuditInfoKey] = auditInfo
|
|
}
|
|
if rootInfo := copyLogOtherMap(o.rootInfo); len(rootInfo) > 0 {
|
|
result[logOtherRootInfoKey] = rootInfo
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (o *LogOther) jsonString() string {
|
|
if o == nil {
|
|
return ""
|
|
}
|
|
data, err := common.Marshal(o.toMap())
|
|
if err != nil {
|
|
common.SysError("failed to marshal log other: " + err.Error())
|
|
return ""
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
// JSONString returns the complete stored representation, including all
|
|
// privileged scopes. API projections must use the role-specific formatter.
|
|
func (o *LogOther) JSONString() string {
|
|
return o.jsonString()
|
|
}
|
|
|
|
// Snapshot returns a detached top-level view for tests and read-only
|
|
// inspection. Mutating it cannot add or replace fields in LogOther.
|
|
func (o *LogOther) Snapshot() map[string]any {
|
|
if o == nil {
|
|
return nil
|
|
}
|
|
return o.toMap()
|
|
}
|
|
|
|
func (o *LogOther) MarshalJSON() ([]byte, error) {
|
|
return common.Marshal(o.toMap())
|
|
}
|
|
|
|
func normalizeLegacyRejectReason(values map[string]json.RawMessage) bool {
|
|
rejectReason, ok := values["reject_reason"]
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
adminInfo := make(map[string]json.RawMessage)
|
|
if rawAdminInfo, exists := values[logOtherAdminInfoKey]; exists {
|
|
_ = common.Unmarshal(rawAdminInfo, &adminInfo)
|
|
}
|
|
if adminInfo == nil {
|
|
adminInfo = make(map[string]json.RawMessage)
|
|
}
|
|
if _, exists := adminInfo["reject_reason"]; !exists {
|
|
adminInfo["reject_reason"] = rejectReason
|
|
}
|
|
encodedAdminInfo, err := common.Marshal(adminInfo)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
values[logOtherAdminInfoKey] = encodedAdminInfo
|
|
delete(values, "reject_reason")
|
|
return true
|
|
}
|
|
|
|
// Expose only the provider's model declaration and a bounded reason to the
|
|
// log owner. The full attempt trail remains under admin_info.
|
|
func projectResponseModelRejection(values map[string]json.RawMessage) bool {
|
|
var code string
|
|
if err := common.Unmarshal(values["error_code"], &code); err != nil || code != "response_model_rejected" {
|
|
return false
|
|
}
|
|
var admin struct {
|
|
RequestDetails struct {
|
|
ModelGuard struct {
|
|
Attempts []struct {
|
|
Expected string `json:"expected_model"`
|
|
Returned string `json:"returned_model"`
|
|
Reason string `json:"reason"`
|
|
} `json:"attempts"`
|
|
} `json:"model_guard"`
|
|
} `json:"request_details"`
|
|
}
|
|
if err := common.Unmarshal(values[logOtherAdminInfoKey], &admin); err != nil || len(admin.RequestDetails.ModelGuard.Attempts) == 0 {
|
|
return false
|
|
}
|
|
attempts := admin.RequestDetails.ModelGuard.Attempts
|
|
last := attempts[len(attempts)-1]
|
|
reason := last.Reason
|
|
if reason != "confirmation_timeout" && reason != "missing_model" && reason != "confirmation_buffer_limit" && reason != "model_mismatch" {
|
|
return false
|
|
}
|
|
result, err := common.Marshal(reason)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
values["model_guard_result"] = result
|
|
for i := len(attempts) - 1; i >= 0; i-- {
|
|
entry := attempts[i]
|
|
if entry.Returned == "" || entry.Expected == "" {
|
|
continue
|
|
}
|
|
observation, err := common.Marshal(map[string]any{
|
|
"requested_model": entry.Expected,
|
|
"upstream_model": entry.Expected,
|
|
"returned_model": entry.Returned,
|
|
"mismatch": entry.Returned != entry.Expected,
|
|
})
|
|
if err == nil {
|
|
values["response_model"] = observation
|
|
}
|
|
break
|
|
}
|
|
return true
|
|
}
|
|
|
|
// formatLogOtherJSON applies the role projection while keeping untouched JSON
|
|
// values as RawMessage. This preserves integers larger than JavaScript's safe
|
|
// range instead of round-tripping them through float64.
|
|
func formatLogOtherJSON(value string, visibility logOtherVisibility) string {
|
|
if value == "" {
|
|
return ""
|
|
}
|
|
|
|
var values map[string]json.RawMessage
|
|
if err := common.UnmarshalJsonStr(value, &values); err != nil {
|
|
if visibility == logOtherVisibilityRoot {
|
|
return value
|
|
}
|
|
return "{}"
|
|
}
|
|
|
|
changed := false
|
|
changed = projectResponseModelRejection(values)
|
|
if visibility == logOtherVisibilityUser {
|
|
for _, key := range []string{logOtherAdminInfoKey, logOtherRootInfoKey, logOtherAuditInfoKey} {
|
|
if _, exists := values[key]; exists {
|
|
delete(values, key)
|
|
changed = true
|
|
}
|
|
}
|
|
for _, key := range legacySensitiveLogOtherKeys {
|
|
if _, exists := values[key]; exists {
|
|
delete(values, key)
|
|
changed = true
|
|
}
|
|
}
|
|
} else {
|
|
changed = normalizeLegacyRejectReason(values) || changed
|
|
if visibility == logOtherVisibilityAdmin {
|
|
if _, exists := values[logOtherRootInfoKey]; exists {
|
|
delete(values, logOtherRootInfoKey)
|
|
changed = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if !changed {
|
|
return value
|
|
}
|
|
formatted, err := common.Marshal(values)
|
|
if err != nil {
|
|
if visibility == logOtherVisibilityRoot {
|
|
return value
|
|
}
|
|
return "{}"
|
|
}
|
|
return string(formatted)
|
|
}
|