Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .agents/upstream-sync/new-api-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# New API Upstream Sync Log

This file tracks selective synchronization from upstream New API into MAX-API. It is a durable project workflow record, not a temporary investigation note.

## Current Baseline

- Upstream checkout: `new-api/new-api`
- Upstream remote: `https://github.com/QuantumNous/new-api.git`
- Last reviewed upstream commit: `25f998595d2da4ac9c749f3eae8fffcf9047bc3e`
- Last reviewed upstream tag: `v1.0.0-rc.15`
- MAX-API branch reviewed: `cscitech`
- MAX-API commit reviewed: `477e4809bac3da3584dfec17be97c991fec78ba3`
- Last review date: `2026-06-29`

## Sync Rules

- Do not wholesale copy upstream files.
- Preserve MAX-API branding, package/import paths, billing, quota, relay retry, security, and UI customizations.
- Prioritize security fixes, billing/log correctness, relay compatibility, database migrations, and operational reliability.
- Keep scratch notes and generated diffs under `.tmp/`; keep this durable sync log in `.agents/upstream-sync/`.
- For frontend user-facing text, update all supported i18n locales when new strings are introduced.

## Status Values

- `pending`: not yet analyzed
- `accepted`: selected for porting
- `ported`: ported into MAX-API
- `skipped`: intentionally not merged
- `superseded`: MAX-API already has equivalent or stronger behavior
- `blocked`: needs a decision or prerequisite

## Review Queue

| Upstream commit/range | Category | Summary | Status | MAX action | Verification | Notes |
| --- | --- | --- | --- | --- | --- | --- |
| `0b48ad86`, `626dadb5` | security, frontend | Custom HTML/Markdown rendering now uses shared `RichContent`/`HtmlContent`; raw HTML is sanitized before rendering on Home, About, Legal, notification popover, and announcement detail content. | ported | Added MAX-API `HtmlContent` and `RichContent`, replaced unsafe `dangerouslySetInnerHTML` paths in public content pages, sandboxed public content iframes, and reused existing `getRenderableContentKind`. | `bun run typecheck`; targeted Node tests for `html-content`, `markdown`, and `renderable-content`. | Highest-priority fix from the 2026-06-29 upstream review. Preserve MAX-API public page layout and branding while porting. |
| `3a506f50`, `2d5a0416` | relay, compatibility | Harden Chat-to-Responses conversion and add Responses-to-Chat/Gemini Responses support. | pending | Evaluate after security rendering fix. | - | Likely conflicts with MAX-API empty completion retry and billing/log behavior. |
| `df44a75d` | bugfix, logs | Adapt ClickHouse log LIKE filters. | pending | Evaluate if ClickHouse log DB is enabled or supported in MAX-API deployments. | - | Current MAX-API still has generic `LIKE ? ESCAPE '!'` in `model/log.go`. |
| `d10fc762` | bugfix, tasks | Attribute async task usage log to the initiating node. | pending | Port narrowly through task private data and task billing log attribution. | - | Important for multi-node deployments. |
| `4aee5f7d` | permissions, admin | Better admin permissions/RBAC for channels and users. | pending | Needs design review before porting. | - | Large change touching backend authz, routes, models, and frontend permissions. |
| `966af88e` | frontend, playground | Improve Playground chat experience and Markdown rendering. | pending | Evaluate after preserving MAX-API one-click clear behavior from `477e4809`. | - | Large frontend refactor. |
| `25f99859`, `1d166532` | frontend, channels | Refine channel management UI and channel drawer layout. | pending | Evaluate after higher-priority fixes. | - | Large channel drawer diff; likely conflicts with MAX-API custom channel UX. |

## Incremental Sync Procedure

1. Fetch upstream in `new-api/new-api`.
2. Read `Last reviewed upstream commit` from this file.
3. Compare only `last_reviewed..origin/main`.
4. Classify each upstream commit into security, bugfix, relay, billing, database, frontend, ops, docs, or tooling.
5. Update the Review Queue with status and rationale before porting.
6. Port only `accepted` items, preserving MAX-API behavior.
7. Record verification commands and any skipped/conflicting areas.
8. Advance `Last reviewed upstream commit` only after the range has been analyzed.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ tiktoken_cache
plans
.claude
.cursor
.agents
.agents/*
!.agents/upstream-sync/
!.agents/upstream-sync/**
.codex
.tmp

Expand Down
41 changes: 36 additions & 5 deletions model/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,27 @@ type Log struct {
RequestId string `json:"request_id,omitempty" gorm:"type:varchar(64);index:idx_logs_request_id;default:''"`
UpstreamRequestId string `json:"upstream_request_id,omitempty" gorm:"type:varchar(128);index:idx_logs_upstream_request_id;default:''"`
Other string `json:"other"`
IsRetry bool `json:"is_retry" gorm:"default:false;index"`
LogId int `json:"log_id,omitempty" gorm:"-"`
}

func (log *Log) BeforeCreate(*gorm.DB) error {
log.syncRetryMarker()
return nil
}

func (log *Log) BeforeSave(*gorm.DB) error {
log.syncRetryMarker()
return nil
}

func (log *Log) syncRetryMarker() {
if log == nil {
return
}
log.IsRetry = logOtherHasRetryMarker(log.Other)
}

// don't use iota, avoid change log type value
const (
LogTypeUnknown = 0
Expand All @@ -78,11 +96,7 @@ func applyLogTypeFilter(tx *gorm.DB, logType int) *gorm.DB {
}

func applyRetryLogFilter(tx *gorm.DB) *gorm.DB {
retryPatterns := []string{
`%"retry_log":true%`,
`%"empty_retry":true%`,
}
return tx.Where("(logs.other LIKE ? OR logs.other LIKE ?)", retryPatterns[0], retryPatterns[1])
return tx.Where("logs.is_retry = ?", true)
}

func applyLogFilter(tx *gorm.DB, filter string) *gorm.DB {
Expand Down Expand Up @@ -164,6 +178,23 @@ func stripLogsAuditContent(logs []*Log) {
}
}

func logOtherHasRetryMarker(other string) bool {
if strings.TrimSpace(other) == "" {
return false
}
otherMap, err := common.StrToMap(other)
if err != nil || otherMap == nil {
return false
}
if retryLog, ok := otherMap["retry_log"].(bool); ok && retryLog {
return true
}
if emptyRetry, ok := otherMap["empty_retry"].(bool); ok && emptyRetry {
return true
}
return false
}

func formatUserLog(log *Log) {
if log == nil {
return
Expand Down
242 changes: 242 additions & 0 deletions model/log_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package model

import (
"os"
"testing"
"time"

"github.com/MAX-API-Next/MAX-API/common"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)

func withLogAuditSettings(t *testing.T, requestEnabled bool, responseEnabled bool) {
Expand Down Expand Up @@ -70,6 +73,245 @@ func TestSumUsedQuotaRetryFilter(t *testing.T) {
require.Equal(t, 71, stat.Tpm)
}

func TestRetryFilterIgnoresNestedRetryMarker(t *testing.T) {
require.NoError(t, LOG_DB.Where("1 = 1").Delete(&Log{}).Error)
t.Cleanup(func() {
require.NoError(t, LOG_DB.Where("1 = 1").Delete(&Log{}).Error)
})

logs := []Log{
{
UserId: 1,
CreatedAt: time.Now().Unix() - 10,
Type: LogTypeConsume,
Quota: 300,
PromptTokens: 3,
CompletionTokens: 7,
Other: common.MapToJsonStr(map[string]interface{}{
"admin_info": map[string]interface{}{
"request_content": `user prompt literally contains "retry_log":true`,
"retry_log": true,
},
}),
},
{
UserId: 1,
CreatedAt: time.Now().Unix() - 20,
Type: LogTypeConsume,
Quota: 200,
PromptTokens: 5,
CompletionTokens: 11,
Other: common.MapToJsonStr(map[string]interface{}{
"retry_log": true,
}),
},
}
require.NoError(t, LOG_DB.Create(&logs).Error)

got, total, err := GetAllLogs(LogTypeUnknown, LogFilterRetry, 0, 0, "", "", "", 0, 10, 0, "", "", "")
require.NoError(t, err)
require.EqualValues(t, 1, total)
require.Len(t, got, 1)
require.Equal(t, logs[1].Id, got[0].Id)

stat, err := SumUsedQuota(LogTypeUnknown, LogFilterRetry, 0, 0, "", "", "", 0, "")
require.NoError(t, err)
require.Equal(t, 200, stat.Quota)
require.Equal(t, 1, stat.Rpm)
require.Equal(t, 16, stat.Tpm)
}

func TestBackfillLogRetryMarkerUsesTopLevelMarkersOnly(t *testing.T) {
markerKey := logRetryMarkerBackfillCompletionKey()
require.NoError(t, LOG_DB.Where("1 = 1").Delete(&Log{}).Error)
require.NoError(t, DB.Where(commonKeyCol+" = ?", markerKey).Delete(&Option{}).Error)
t.Cleanup(func() {
require.NoError(t, LOG_DB.Where("1 = 1").Delete(&Log{}).Error)
require.NoError(t, DB.Where(commonKeyCol+" = ?", markerKey).Delete(&Option{}).Error)
})

logs := []Log{
{
UserId: 1,
Type: LogTypeConsume,
Other: common.MapToJsonStr(map[string]interface{}{
"retry_log": true,
}),
},
{
UserId: 1,
Type: LogTypeConsume,
Other: common.MapToJsonStr(map[string]interface{}{
"admin_info": map[string]interface{}{
"retry_log": true,
},
}),
},
}
require.NoError(t, LOG_DB.Create(&logs).Error)
require.NoError(t, LOG_DB.Model(&Log{}).Where("1 = 1").UpdateColumn("is_retry", false).Error)

require.NoError(t, backfillLogRetryMarker())

var reloaded []Log
require.NoError(t, LOG_DB.Order("id asc").Find(&reloaded).Error)
require.Len(t, reloaded, 2)
require.True(t, reloaded[0].IsRetry)
require.False(t, reloaded[1].IsRetry)
}

func TestBackfillLogRetryMarkerSkipsAfterCompletionMarker(t *testing.T) {
markerKey := logRetryMarkerBackfillCompletionKey()
require.NoError(t, LOG_DB.Where("1 = 1").Delete(&Log{}).Error)
require.NoError(t, DB.Where(commonKeyCol+" = ?", markerKey).Delete(&Option{}).Error)
t.Cleanup(func() {
require.NoError(t, LOG_DB.Where("1 = 1").Delete(&Log{}).Error)
require.NoError(t, DB.Where(commonKeyCol+" = ?", markerKey).Delete(&Option{}).Error)
})

log := Log{
UserId: 1,
Type: LogTypeConsume,
Other: common.MapToJsonStr(map[string]interface{}{
"retry_log": true,
}),
}
require.NoError(t, LOG_DB.Create(&log).Error)
require.NoError(t, LOG_DB.Model(&Log{}).Where("1 = 1").UpdateColumn("is_retry", false).Error)
require.NoError(t, backfillLogRetryMarker())

var marker Option
require.NoError(t, DB.First(&marker, commonKeyCol+" = ?", markerKey).Error)
require.Equal(t, "true", marker.Value)

require.NoError(t, LOG_DB.Model(&Log{}).Where("1 = 1").UpdateColumn("is_retry", false).Error)
require.NoError(t, backfillLogRetryMarker())

var reloaded Log
require.NoError(t, LOG_DB.First(&reloaded, log.Id).Error)
require.False(t, reloaded.IsRetry)
}

func TestBackfillLogRetryMarkerCompletionIsScopedToLogDBIdentity(t *testing.T) {
originalDB := DB
originalLOGDB := LOG_DB
originalLogSQLType := common.LogSqlType
t.Cleanup(func() {
DB = originalDB
LOG_DB = originalLOGDB
common.LogSqlType = originalLogSQLType
initCol()
})

mainDB := newRetryBackfillTestDB(t, &Option{})
logOneDB := newRetryBackfillTestDB(t, &Log{})
logTwoDB := newRetryBackfillTestDB(t, &Log{})

DB = mainDB
common.LogSqlType = common.DatabaseTypeSQLite

t.Setenv("LOG_SQL_DSN", "sqlite://log-one")
initCol()
LOG_DB = logOneDB
logOne := Log{
UserId: 1,
Type: LogTypeConsume,
Other: common.MapToJsonStr(map[string]interface{}{
"retry_log": true,
}),
}
require.NoError(t, LOG_DB.Create(&logOne).Error)
require.NoError(t, LOG_DB.Model(&Log{}).Where("1 = 1").UpdateColumn("is_retry", false).Error)
require.NoError(t, backfillLogRetryMarker())

var reloaded Log
require.NoError(t, logOneDB.First(&reloaded, logOne.Id).Error)
require.True(t, reloaded.IsRetry)

require.NoError(t, os.Setenv("LOG_SQL_DSN", "sqlite://log-two"))
initCol()
LOG_DB = logTwoDB
logTwo := Log{
UserId: 1,
Type: LogTypeConsume,
Other: common.MapToJsonStr(map[string]interface{}{
"retry_log": true,
}),
}
require.NoError(t, LOG_DB.Create(&logTwo).Error)
require.NoError(t, LOG_DB.Model(&Log{}).Where("1 = 1").UpdateColumn("is_retry", false).Error)

require.NoError(t, backfillLogRetryMarker())

reloaded = Log{}
require.NoError(t, logTwoDB.First(&reloaded, logTwo.Id).Error)
require.True(t, reloaded.IsRetry)
}

func TestLogRetryMarkerCompletionKeyUsesHashedLogDBIdentity(t *testing.T) {
originalLogSQLType := common.LogSqlType
t.Cleanup(func() {
common.LogSqlType = originalLogSQLType
})

common.LogSqlType = common.DatabaseTypeMySQL
t.Setenv("LOG_SQL_DSN", "user:secret@tcp(log-one:3306)/logs")
firstKey := logRetryMarkerBackfillCompletionKey()
require.NotEqual(t, logRetryMarkerBackfillOptionKey, firstKey)
require.NotContains(t, firstKey, "secret")
require.NotContains(t, firstKey, "log-one")

require.NoError(t, os.Setenv("LOG_SQL_DSN", "user:secret@tcp(log-two:3306)/logs"))
secondKey := logRetryMarkerBackfillCompletionKey()
require.NotEqual(t, firstKey, secondKey)
}

func TestLogRetryMarkerIsRecomputedWhenOtherChanges(t *testing.T) {
require.NoError(t, LOG_DB.Where("1 = 1").Delete(&Log{}).Error)
t.Cleanup(func() {
require.NoError(t, LOG_DB.Where("1 = 1").Delete(&Log{}).Error)
})

log := Log{
UserId: 1,
Type: LogTypeConsume,
Other: common.MapToJsonStr(map[string]interface{}{
"retry_log": true,
}),
}
require.NoError(t, LOG_DB.Create(&log).Error)
require.True(t, log.IsRetry)

log.Other = common.MapToJsonStr(map[string]interface{}{
"admin_info": map[string]interface{}{
"use_channel": []string{"1"},
},
})
require.NoError(t, LOG_DB.Save(&log).Error)

var reloaded Log
require.NoError(t, LOG_DB.First(&reloaded, log.Id).Error)
require.False(t, reloaded.IsRetry)
}

func newRetryBackfillTestDB(t *testing.T, models ...interface{}) *gorm.DB {
t.Helper()

db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)

sqlDB, err := db.DB()
require.NoError(t, err)
sqlDB.SetMaxOpenConns(1)
t.Cleanup(func() {
require.NoError(t, sqlDB.Close())
})

require.NoError(t, db.AutoMigrate(models...))

return db
}

func createRetryFilterLogs(t *testing.T) []Log {
t.Helper()

Expand Down
Loading
Loading